From toUpperCase to Smart Conversion: Building a Multi-Format Case Converter
From toUpperCase to Smart Conversion: Building a Multi-Format Case Converter#
While refactoring a project recently, I noticed inconsistent variable naming - some used camelCase, others snake_case, and a few kebab-case. Manual conversion was tedious, so I built a converter tool and documented the implementation approach.
Nine Case Formats#
Let’s first review common naming conventions:
| Format | Example | Usage |
|---|---|---|
| UPPER | HELLO WORLD | Constants, emphasis |
| lower | hello world | Plain text |
| Title Case | Hello World | Article titles, UI headings |
| Sentence case | Hello world | Paragraph beginnings |
| camelCase | helloWorld | JavaScript variables, functions |
| PascalCase | HelloWorld | JavaScript classes, React components |
| snake_case | hello_world | Python variables, database fields |
| kebab-case | hello-world | CSS classes, URL paths |
| CONSTANT_CASE | HELLO_WORLD | Global constants, environment variables |
Each format has its use case. The core of conversion lies in tokenization and reassembly.
Implementing Basic Conversions#
Upper and Lower Case#
The simplest two, using native methods:
function toUpperCase(text: string): string {
return text.toUpperCase()
}
function toLowerCase(text: string): string {
return text.toLowerCase()
}
Title Case#
Capitalize the first letter of each word:
function toTitleCase(text: string): string {
return text.toLowerCase().replace(/\b\w/g, char => char.toUpperCase())
}
\b matches word boundaries, \w matches letters, digits, and underscores. This regex finds the first letter of each word and capitalizes it.
But there’s a catch: abbreviations get mishandled. For example, JSON.parse becomes Json.Parse. To preserve abbreviations, you’d need an exception list.
Sentence Case#
Only capitalize the first letter of sentences:
function toSentenceCase(text: string): string {
return text.toLowerCase().replace(/(^|[.!?]\s+)([a-z])/g, (_, sep, char) => {
return sep + char.toUpperCase()
})
}
This regex matches three scenarios:
^- Start of string[.!?]\s+- Period/question mark/exclamation mark + space
Note \s+ instead of \s to handle multiple spaces.
Programming Naming Style Conversions#
This part is more complex, requiring both tokenization and reassembly.
camelCase and PascalCase#
Core idea: treat all non-alphanumeric characters as separators, capitalize the following character:
function toCamelCase(text: string): string {
return text
.toLowerCase()
.replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase())
.replace(/^([A-Z])/, char => char.toLowerCase())
}
function toPascalCase(text: string): string {
return text
.toLowerCase()
.replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase())
.replace(/^([a-z])/, char => char.toUpperCase())
}
[^a-zA-Z0-9]+(.) matches “a sequence of non-alphanumeric characters + one character”, then capitalizes that character.
Examples:
hello world→helloWorlduser_name→userNameUSER-NAME→userName
The final step handles the first letter: lowercase for camelCase, uppercase for PascalCase.
snake_case and kebab-case#
Strategy: first insert separators before uppercase letters, then normalize:
function toSnakeCase(text: string): string {
return text
.replace(/([A-Z])/g, '_$1') // Insert underscore before uppercase
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_') // Non-alphanumeric to underscore
.replace(/^_+|_+$/g, '') // Remove leading/trailing underscores
.replace(/_+/g, '_') // Merge multiple underscores
}
function toKebabCase(text: string): string {
return text
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/-+/g, '-')
}
Key point: ([A-Z])/g matches each uppercase letter, '_$1' inserts a separator before it.
Examples:
helloWorld→hello_worldHelloWorld→_hello_world→hello_worldHELLO_WORLD→_h_e_l_l_o__w_o_r_l_d→hello_world
CONSTANT_CASE#
Uppercase version of snake_case:
function toConstantCase(text: string): string {
return text
.replace(/([A-Z])/g, '_$1')
.toUpperCase()
.replace(/[^A-Z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
.replace(/_+/g, '_')
}
Handling Edge Cases#
Real-world usage has many pitfalls:
1. Consecutive Separators#
hello__world (two underscores) should become hello_world, not hello__world.
Solution: use replace(/_+/g, '_') at the end to merge consecutive separators.
2. Leading/Trailing Separators#
_hello_world_ should become hello_world, not _hello_world_.
Solution: replace(/^_+|_+$/g, '') removes leading/trailing separators.
3. Number Handling#
How should user2name be handled?
- snake_case:
user2name(numbers preserved) - camelCase:
user2name(numbers aren’t separators)
My implementation treats numbers as part of words. If you need user_2_name, insert separators around numbers:
function toSnakeCaseWithNumbers(text: string): string {
return text
.replace(/([A-Z])/g, '_$1')
.replace(/(\d+)/g, '_$1') // Add separators around numbers
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
.replace(/_+/g, '_')
}
4. Unicode Characters#
toUpperCase() and toLowerCase() handle Unicode well:
'istanbul'.toUpperCase() // 'ISTANBUL'
But regex \w only matches [A-Za-z0-9_], not Unicode letters. For Chinese variable names, use Unicode property escapes:
function toTitleCaseUnicode(text: string): string {
return text.toLowerCase().replace(/\p{L}/gu, char => char.toUpperCase())
}
\p{L} matches letters from any language, u flag enables Unicode mode.
Performance Optimization#
1. Avoid Multiple Traversals#
The above implementation uses multiple replace calls, each traversing the entire string. Merge into one regex:
function toSnakeCaseOptimized(text: string): string {
return text
.replace(/([A-Z])|([^a-zA-Z0-9]+)/g, (match, upper, sep) => {
if (upper) return '_' + upper.toLowerCase()
if (sep) return '_'
return match
})
.replace(/^_+|_+$/g, '')
.replace(/_+/g, '_')
}
One regex handles both uppercase letters and separators.
2. Cache Results#
If the same string is converted multiple times, use a Map cache:
const cache = new Map<string, Map<string, string>>()
function convertCase(text: string, type: string): string {
if (!cache.has(text)) {
cache.set(text, new Map())
}
const textCache = cache.get(text)!
if (textCache.has(type)) {
return textCache.get(type)!
}
const result = convertCaseImpl(text, type)
textCache.set(type, result)
return result
}
3. Web Worker#
For batch conversions, use a Web Worker to avoid blocking UI:
// worker.ts
self.onmessage = (e) => {
const { texts, type } = e.data
const results = texts.map(text => convertCase(text, type))
self.postMessage(results)
}
// main.tsx
const worker = new Worker('worker.ts')
worker.postMessage({ texts: largeTextArray, type: 'camel' })
worker.onmessage = (e) => {
setResults(e.data)
}
Real-World Use Cases#
Code Refactoring#
Unify old code variable names to new style:
// Old code
const user_name = 'John'
const user_age = 25
// Convert to camelCase
const userName = 'John'
const userAge = 25
API Data Transformation#
Backend returns snake_case, frontend needs camelCase:
function transformKeys(obj: any, caseType: 'camel' | 'snake'): any {
if (Array.isArray(obj)) {
return obj.map(item => transformKeys(item, caseType))
}
if (obj && typeof obj === 'object') {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
convertCase(key, caseType),
transformKeys(value, caseType)
])
)
}
return obj
}
CSS Class Name Generation#
Generate BEM class names from component names:
function generateBEM(componentName: string): string {
const block = toKebabCase(componentName)
return {
block,
element: (name: string) => `${block}__${toKebabCase(name)}`,
modifier: (name: string) => `${block}--${toKebabCase(name)}`
}
}
const bem = generateBEM('UserProfile')
bem.block // 'user-profile'
bem.element('avatar') // 'user-profile__avatar'
bem.modifier('large') // 'user-profile--large'
Final Result#
Based on these ideas, I built an online tool: Case Converter
Key features:
- Support for 9 format conversions
- Real-time preview
- One-click copy
- Batch conversion support
The implementation isn’t complex, but handling edge cases properly requires attention. Hope this helps!
Related tools: Diff Checker | Regex Tester