16
\$\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\$

3 Answers 3

12
\$\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\$
2
  • \$\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\$ May 31, 2019 at 18:20
  • 1
    \$\begingroup\$ Good catch, fixed. \$\endgroup\$
    – ggorlen
    May 31, 2019 at 19:08
9
\$\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\$
2
\$\begingroup\$

Instead of writing and maintaining this function, one could use this publicly available package: to-case.

Sample usage:

import to from "to-case";

titleCaseString = to.capital(camelCaseString)
\$\endgroup\$
4
  • 2
    \$\begingroup\$ This site is about improving existing code. While this might be a good answer on stackoverflow, it is not a good answer on Code Review. A good answer on Code Review contains at least one insightful observation about the code. Alternate code only solutions are considered poor answers and may be down voted or deleted by the community. Please read How do I write a good answer. \$\endgroup\$
    – pacmaninbw
    Nov 22, 2023 at 14:16
  • 2
    \$\begingroup\$ @pacmaninbw Using an in-built function instead of rolling your own would be an obvious improvement if the output is identical, don't you think? The answer could be improved, definitely, but it's effectively the core of something similar to what I did here in '15. \$\endgroup\$
    – Mast
    Nov 22, 2023 at 16:01
  • \$\begingroup\$ @pacmaninbw while it may not be a complete consensus there is much support for the viewpoint in this accepted answer to Is simply telling someone to use an API an appropriate code review? which pertains to this issue. \$\endgroup\$ Nov 22, 2023 at 16:32
  • \$\begingroup\$ @sᴀᴍ-onᴇᴌᴀ thanks for the edit 👍 \$\endgroup\$ Nov 23, 2023 at 16:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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