15
\$\begingroup\$

I'm trying to take camelCase and PascalCase strings into a function and spit them out as Title Case. This function also needs to be able to handle odd PascalCase strings with capitalized abbreviations such as "CDReceiverBox" and return a readable string - "CD Receiver Box".

My current working solution:

function splitCamelCase(camelCaseString) {
    const result = camelCaseString
        .replace(/([A-Z][a-z])/g, " $1")
        .replace(/([A-Z]+)/g, " $1")
        .replace(/ +/g, " ")
        .replace(/^ +/g, "");

    return result.charAt(0).toUpperCase() + result.slice(1);
}

I would like to condense the amount of replace statements I'm using by at least combining the first two replace statements and the last two together since they are semi similar. The more concise I can make this the better.

CodePen: https://codepen.io/andrewgarrison/pen/dEQrMy

\$\endgroup\$
10
\$\begingroup\$

I like your solution quite a bit. It's clear, easy to read and I don't see any bugs.

There are many ways to condense the replace calls as you mention, but I think you're at a point where such changes can easily have a disproportionate impact on readability. That's good--it means the code is already pretty optimal from that standpoint.

For example, here's a one-shot replace using alternation, but its merit is debatable:

const splitCamelCase = s => s.replace(
    /^[a-z]|^([A-Z]+)(?=[A-Z]|$)|([A-Z])+(?=[A-Z]|$)|([A-Z])(?=[a-z]+)/g,
    m => " " + m.toUpperCase()
  ).trim()
;

The idea here is to enumerate each scenario, join the patterns with |s, and provide an arrow function to handle the addition of a space and a capital letter for each match.

With the two extremes in mind, I prefer a balanced approach such as:

const splitCamelCase = s =>
  s.replace(/([A-Z][a-z])/g, " $1")
   .replace(/\s*([A-Z]+)/g, " $1")
   .replace(/./, m => m.toUpperCase())
   .trim()
;

or perhaps

const splitCamelCase = s =>
  s.replace(/([A-Z][a-z]|[A-Z]+(?=[A-Z]|$))/g, " $1")
   .replace(/./, m => m.toUpperCase())
   .trim()
;

These should offer ideas as far as how far you want to go in making the succinctness versus readability tradeoff. But, failing the possibility of a shortcut I might have overlooked, keeping your code basically as-is seems like a fine option to me.

If it's performance you're after in reducing replace calls, there's no guarantee that fewer calls will translate into better performance. Under the hood, the regex engine may make more passes to compensate; you can benchmark and tweak using a debugger like regex101. For performance, it's likely best to avoid regex entirely and write a single-pass loop by hand.

Here's a test runner:

const splitCamelCase = s =>
  s.replace(/([A-Z][a-z]|[A-Z]+(?=[A-Z]|$))/g, " $1")
   .replace(/./, m => m.toUpperCase())
   .trim()
;

[
  "AAABbbbbCcDddEEFffGGHhIiJ",
  "AaBbCcDDEeFGgHHHH",
  "CDBoomBoxAAAABbbbCCC",
  "CDBoomBox",
  "camelCase",
  "camel",
  "Camel",
  "c",
  "C",
  "Aa",
  "AA",
  "aa",
  "AAA",
  "aB", 
  "aBC",
  "aBCc",
  "",
].forEach(test => 
  console.log(
    splitCamelCaseOriginal(test) === splitCamelCase(test) 
      ? `'${test}' -> '${splitCamelCase(test)}'` 
      : "TEST FAILED"
  )
);

function splitCamelCaseOriginal(camelCaseString) {
    const result = camelCaseString
        .replace(/([A-Z][a-z])/g, " $1")
        .replace(/([A-Z]+)/g, " $1")
        .replace(/ +/g, " ")
        .replace(/^ +/g, "");

    return result.charAt(0).toUpperCase() + result.slice(1);
}

\$\endgroup\$
  • \$\begingroup\$ FWIW, your code gives different results from the OP's for inputs like aB, aBC or aBCd that start with a lowercase letter followed by an uppercase letter. That's the one situation where it matters whether you capitalize the first letter before or after adding the spaces. \$\endgroup\$ – Ilmari Karonen May 31 at 18:20
  • 1
    \$\begingroup\$ Good catch, fixed. \$\endgroup\$ – ggorlen May 31 at 19:08
8
\$\begingroup\$

You could save one .replace() call by replacing the last two with:

.replace(/(^| ) +/g, "$1")

which both removes leading spaces and collapses multiple consecutive spaces to one anywhere else in the string. However, I'm not 100% sure that you should, since it's not really clear which way is more efficient in practice, and your way seems more readable anyway.

If you do keep the two calls separate, however, you should optimize the first regexp to / +/g (with two spaces before to + sign) or / {2,}/g (which means "two or more spaces"), to avoid unnecessarily matching and replacing single spaces. Also, swapping the order of the last two calls could improve performance slightly in cases where the only extra spaces to be removed are at the beginning of the string.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.