Skip to content

Regex to consolidate multiple rules

I’m looking at optimising my string manipulation code and consolidating all of my replaceAll‘s to just one pattern if possible


  • strip all special chars except -
  • replace space with -
  • condense consecutive - ‘s to just one -
  • Remove leading and trailing -‘s

My code

public static String slugifyTitle(String value) {
    String slugifiedVal = null;
    if (StringUtils.isNotEmpty(value))
        slugifiedVal = value
                .replaceAll("[ ](?=[ ])|[^-A-Za-z0-9 ]+", "") // strips all special chars except -
                .replaceAll("\s+", "-") // converts spaces to -
                .replaceAll("--+", "-"); // replaces consecutive -'s with just one -

    slugifiedVal = StringUtils.stripStart(slugifiedVal, "-"); // strips leading -
    slugifiedVal = StringUtils.stripEnd(slugifiedVal, "-"); // strips trailing -

    return slugifiedVal;

Does the job but obviously looks shoddy.

My test assertions –

Heading with symbols *~!@#$%^&()_+-=[]{};',.<>?/ ==> heading-with-symbols
Heading with an asterisk* ==> heading-with-an-asterisk
Custom-id-&-stuff ==> custom-id-stuff
--Custom-id-&-stuff-- ==> custom-id-stuff



Disclaimer: I don’t think a regex approach to this problem is wrong, or that this is an objectively better approach. I am merely presenting an alternative approach as food for thought.

I have a tendency against regex approaches to problems where you have to ask how to solve with regex, because that implies you’re going to struggle to maintain that solution in the future. There is an opacity to regexes where “just do this” is obvious, when you know just to do this.

Some problems typically solved with regex, like this one, can be solved using imperative code. It tends to be more verbose, but it uses simple, apparent, code constructs; it’s easier to debug; and can be faster because it doesn’t involve the full “machinery” of the regex engine.

static String slugifyTitle(String value) {
    boolean appendHyphen = false;
    StringBuilder sb = new StringBuilder(value.length());

    // Go through value one character at a time...
    for (int i = 0; i < value.length(); i++) {
      char c = value.charAt(i);

      if (isAppendable(c)) {
        // We have found a character we want to include in the string.

        if (appendHyphen) {
          // We previously found character(s) that we want to append a single
          // hyphen for.
          appendHyphen = false;
      } else if (requiresHyphen(c)) {
        // We want to replace hyphens or spaces with a single hyphen.
        // Only append a hyphen if it's not going to be the first thing in the output.
        // Doesn't matter if this is set for trailing hyphen/whitespace,
        // since we then never hit the "isAppendable" condition.
        appendHyphen = sb.length() > 0;
      } else {
        // Other characters are simply ignored.

    // You can lowercase when appending the character, but `Character.toLowerCase()`
    // recommends using `String.toLowerCase` instead.
    return sb.toString().toLowerCase(Locale.ROOT);

// Some predicate on characters you want to include in the output.
static boolean isAppendable(char c) {
  return (c >= 'A' && c <= 'Z')
      || (c >= 'a' && c <= 'z')
      || (c >= '0' && c <= '9');

// Some predicate on characters you want to replace with a single '-'.
static boolean requiresHyphen(char c) {
  return c == '-' || Character.isWhitespace(c);

(This code is wildly over-commented, for the purpose of explaining it in this answer. Strip out the comments and unnecessary things like the else, it’s actually not super complicated).

9 People found this is helpful