Case Converter
camelCase, snake_case, kebab-case and the rest — with acronyms and digits split the way you meant.
every style appears here as you type
Correct tokenisation is the product
Case conversion itself is a lookup table. What almost every incumbent gets wrong is
splitting the identifier into words in the first place, because the obvious regex
— split on separators, then insert a break between a lowercase letter and an
uppercase one — never fires inside a run of capitals. Measured against exactly
that rule on this machine (Node 24.12): XMLHttpRequest becomes
XMLHttp·Request, one token short of correct;
IOError stays a single token; parseHTML5Document becomes
parse·HTML5Document, the digit run swallowing the
acronym after it.
This page runs four rules, in a fixed order, each measured against the cases
above and against the four identifiers a looser acronym rule breaks:
OAuth2Token, IPv4Address, OAuthToken and
getIDs. The acronym rule needs two or more capitals
before the boundary, not one or more — with just one, OAuth2Token
becomes O·Auth2Token, which is worse than the
incumbent it is trying to fix. A digit before a capitalised word
(HTML5Document, OAuth2Token) is always a boundary; a
lowercase letter before a digit (user2FA) is not, by default, because
the identical shape shows up in md5sum, sha256 and
base64url, which do not want the split. There is no correct default
for that one case, so it is a control next to the token list, not a guess baked
into the page.
What stays wrong on purpose
OAuth, IPad and IPhone correctly stay one
token — a single leading capital before a lowercase run is never a boundary here.
The same shape means XAxis, IError, TCell,
gRPCClient and iOSDevice also stay whole, which is
usually not what was meant. No regex tells the two intents apart; only a lexicon
of known acronyms would, and a lexicon this page does not maintain. The token list
under the input is exactly this rule's output, so a wrong split is visible on the
page rather than silently wrong in whatever gets pasted next.
Case conversion is also not always reversible. 'ß'.toUpperCase()
gives 'SS', and lowercasing that back gives 'ss', not
ß. Turkish and Azeri distinguish a dotted and dotless I that
the ordinary, locale-invariant casing rules do not — running Turkish text through
toUpperCase()/toLowerCase() without the Turkish locale
changes its length. This page flags a line as lossy when its case change does not
round-trip, and carries a Turkish-locale toggle for the one case that needs it.
Nothing here ever leaves the browser tab (verify it yourself).
Do it without this tool
// The four boundary rules, in the order they must run. {2,} in R2 is the whole
// trick — [A-Z]+ regresses OAuth2Token, IPv4Address, OAuthToken and getIDs into
// wrong splits that these four rules, in this order, get right.
const R2 = s => s.replace(/([A-Z]{2,})([A-Z][a-z])/g, '$1 $2'); // XMLHttp -> XML Http
const R1 = s => s.replace(/([a-z])([A-Z])/g, '$1 $2'); // userID -> user ID
const R3 = s => s.replace(/([0-9])([A-Z][a-z])/g, '$1 $2'); // HTML5Doc -> HTML5 Doc
const R4 = s => s.replace(/([a-z])([0-9])/g, '$1 $2'); // user2FA -> user 2FA (opt-in — md5sum, v2, base64url do not want this)
const tokenize = (s, { splitDigits = false } = {}) => {
let out = R3(R1(R2(s)));
if (splitDigits) out = R4(out);
return out.split(/[\s_\-./]+/).filter(Boolean);
};
tokenize('XMLHttpRequest'); // ['XML', 'Http', 'Request']
tokenize('parseHTML5Document'); // ['parse', 'HTML5', 'Document']
tokenize('OAuth2Token'); // ['OAuth2', 'Token'] — {2,} keeps OAuth whole
import re
R2 = lambda s: re.sub(r'([A-Z]{2,})([A-Z][a-z])', r'\1 \2', s)
R1 = lambda s: re.sub(r'([a-z])([A-Z])', r'\1 \2', s)
R3 = lambda s: re.sub(r'([0-9])([A-Z][a-z])', r'\1 \2', s)
R4 = lambda s: re.sub(r'([a-z])([0-9])', r'\1 \2', s)
def tokenize(s, split_digits=False):
out = R3(R1(R2(s)))
if split_digits:
out = R4(out)
return [t for t in re.split(r'[\s_\-./]+', out) if t]
tokenize('XMLHttpRequest') # ['XML', 'Http', 'Request']
# Bulk-rename camelCase identifiers to snake_case across a file — good enough for # the common case, and exactly the naive rule this page's tokeniser improves on # (it will still mis-split an XMLHttpRequest-shaped identifier; check the diff). # Portable across GNU and BSD/macOS sed — verified both, no labels or loops needed. sed -E 's/([a-z0-9])([A-Z])/\1_\2/g' file.js | tr '[:upper:]' '[:lower:]' # The same, restricted to identifiers only (skip strings/comments) needs a real # parser — this one-liner is for a quick pass on a small file, not a codebase.
# Naming conventions by language/style guide, for the row this page is naming: Python (PEP 8) snake_case functions/vars, PascalCase classes, UPPER_SNAKE constants Rust (API guidelines) snake_case fns/vars/modules, PascalCase types, SCREAMING_SNAKE_CASE consts/statics Go camelCase unexported, PascalCase exported (case controls visibility) JavaScript/TypeScript camelCase vars/functions, PascalCase classes/types, UPPER_SNAKE for true constants Java (Google style) camelCase methods/fields, PascalCase classes, CONSTANT_CASE static finals C++ (Google style) PascalCase types/functions, snake_case locals, kConstantCase constants CSS/HTML kebab-case for classes, ids and custom properties URL paths kebab-case or lowercase, rarely camelCase
The specs, which one says what
PEP 8 is the reference for Python's snake_case
functions and variables, PascalCase classes and
UPPER_SNAKE_CASE constants. The Google style guides
(C++, Java) and the Rust API guidelines cover the same ground for
their languages, including the constant-naming split this page's FAQ answers.
Unicode Standard Annex #21 (Case Mappings) is the spec behind
the non-reversible casing rows above — the special-casing tables for
ß, ligatures, and the Turkish I pair.
Renaming is the workflow: JSON string escape/unescape and URL encode/decode for where a renamed identifier often ends up next, and text & JSON diff for checking the rename itself, line by line.
FAQ
Why does XMLHttpRequest become xmlhttp_request on most converters?
Most converters split only on a lowercase-to-uppercase boundary, which never fires inside a run of capitals — so an acronym and the word after it stay glued together. This page adds a second, ordered rule for a run of two or more capitals before a capitalised word, which is what actually separates the acronym first.
Should HTML5Document split into HTML5 and Document?
Yes — a digit before a capitalised word is an unconditional boundary here, so
HTML5Document and OAuth2Token both split at the digit
regardless of any other setting.
Why does XAxis stay one word when XMLHttp splits?
The acronym rule needs a second capitalised word after the capital run to fire;
XMLHttpRequest has one and XAxis does not. The same
protection that correctly keeps OAuth whole keeps XAxis
whole too, whether or not that was wanted — no regex tells the two cases apart.
Why did my identifier change length when I uppercased it?
German's ß and the ligature fi have no matching
single-character form in the other case, so uppercasing then lowercasing them does
not return the original character. This page flags the line rather than silently
returning something that will not round-trip.
What is the Turkish İ/ı problem, and when does it actually bite?
Turkish and Azeri distinguish a dotted and dotless I that the ordinary
casing rules do not, so running Turkish text through the invariant
toUpperCase()/toLowerCase() can change its length. The
tr-locale toggle applies the correct tables instead.
What is SCREAMING_SNAKE_CASE called in each language?
PEP 8 calls it the constants convention without a separate nickname, Rust's API
guidelines name it SCREAMING_SNAKE_CASE explicitly, and the Google
style guides call it CONSTANT_CASE (C++) or the same
SCREAMING_SNAKE_CASE in Java.