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 worldhelloWorld
  • user_nameuserName
  • USER-NAMEuserName

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:

  • helloWorldhello_world
  • HelloWorld_hello_worldhello_world
  • HELLO_WORLD_h_e_l_l_o__w_o_r_l_dhello_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