Every codebase makes you retype the same name in five different shapes. A database column comes back as user_profile_id, but your JavaScript wants userProfileId, the CSS class wants user-profile-id, the env var wants USER_PROFILE_ID, and the class is UserProfileId. I used to do this by hand, one careful edit at a time, and get it subtly wrong often enough to be annoyed. So I built a case converter, and the interesting part wasn't the conversion at all — it was realising the whole problem collapses to a single function.
You can play with the live version here: https://dev48v.infy.uk/solve/day28-case-converter.html
The trap I nearly fell into
My first instinct was to write a function for each direction: camelToSnake, snakeToCamel, kebabToPascal, and so on. That's how most half-finished converters on the internet are built. The problem shows up fast: with N cases you need N times N functions, every one of them slightly different, and adding an eleventh case means writing ten new pairings.
The fix is to route everything through a canonical middle. Split any input into a plain list of words once, then join those words into whatever target you want. Now you need one splitter plus one small join rule per case — N plus one functions instead of N times N. Adding a case is a single line. The splitter is the only hard part, and once it's right, everything downstream is a one-liner.
Splitting is the whole game
Half the input cases hand you the boundaries for free: underscores, hyphens, dots, slashes and spaces are explicit separators. One regex split handles all of them at once, and a filter drops the empty strings that leading or doubled delimiters leave behind.
str.split(/[^A-Za-z0-9]+/).filter(Boolean);
// "my-var_name" -> ["my","var","name"]
The hard half is camelCase and PascalCase, which carry no delimiter. The word boundary is a hump: a lowercase letter or digit immediately followed by an uppercase one. The neat move is not to split there, but to inject a space at every hump and let the delimiter split finish the job.
"getUserName".replace(/([a-z0-9])([A-Z])/g, "$1 $2");
// -> "get User Name"
The bug that taught me about acronyms
The hump rule looked done until I fed it HTTPServer. There's no lowercase-to-uppercase transition at the real boundary — P to S is uppercase to uppercase — so a lazy rule either leaves it whole or, worse, splits it as HTTPS erver. Both are wrong.
The tell is a run of capitals followed by an uppercase-then-lowercase pair. That means an acronym is butting up against a word, and you split before that last capital:
str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
// "HTTPServer" -> "HTTP Server"
Then digits. A digit usually begins a new token, so I inject a space at every letter-to-digit boundary. I deliberately do not split digit-to-letter, which keeps tokens like 2xx or 3d intact — that matches how people actually read them. Put together, HTTPServerError2xx tokenizes to ["HTTP", "Server", "Error", "2xx"], which is exactly what I wanted.
Joining is trivial once you have words
With a clean word list, every case is two decisions: how to transform each word, and what separator to join with.
const cap = w => w[0].toUpperCase() + w.slice(1).toLowerCase();
const CONVERTERS = {
camelCase: ws => ws.map((w, i) => i ? cap(w) : w.toLowerCase()).join(""),
snake_case: ws => ws.map(w => w.toLowerCase()).join("_"),
CONSTANT: ws => ws.map(w => w.toUpperCase()).join("_"),
"Title Case": ws => ws.map(cap).join(" "),
// ...one line per case
};
To convert a whole multi-line list, I split on newlines and tokenize each line independently, so a messy paste of camel, snake and kebab all normalizes into whatever case you pick. I wrote a small test that round-trips user_id through camelCase and back, and checks all ten joins against HTTPServerError2xx — it passes.
The thing that surprised me: some conversions are lossy
I expected everything to be reversible. It isn't. The programmer cases — camel, Pascal, snake, kebab, dot, path — all encode word boundaries unambiguously, so user_id to userId and back is stable forever. But casing normalizes acronyms: once HTTPServer becomes Http Server, there is no way to recover that HTTP was uppercase. The information is gone.
And human-readable cases lose their boundaries the instant they enter a space-free case. "New York" becomes newYork, and the tokenizer will never split that back into New and York — it sees one hump, not a place name. So the rule I ended up with: convert freely among the code cases, but treat Title and Sentence case as a one-way exit.
The full build, with live tokenizing and a copy button per case, is here: https://dev48v.infy.uk/solve/day28-case-converter.html
Top comments (0)