Skip to content

Case Converter — Swift source

Convert text between UPPER, lower, Title, Sentence, camelCase, PascalCase, snake_case, kebab-case, CONSTANT_CASE and slug. Live, one-click copy.

This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.

// case-converter — pure, deterministic text-case transformations.
//
// Language: Swift 5.9 (standard library only)
// Source:   CosmoDev polyglot showcase port of the Case Converter tool.
//           Canonical: src/lib/case.ts (the live TypeScript) plus this
//           tool's python.py / rust.rs reference ports.
// License:  display source — part of CosmoDev's polyglot tool pages.
//
// Split any input into word tokens and re-emit it in upper / lower / title /
// sentence / camel / pascal / snake / kebab / constant / slug case — same
// inputs produce the same outputs as the TypeScript original, including edge
// cases. Swift String case mapping is Unicode-aware (uppercased() /
// lowercased()), matching JavaScript's toUpperCase / toLowerCase; the token
// alphabet itself is ASCII ([A-Za-z0-9]), so the classifiers below gate on
// isASCII exactly like the reference's regex classes.

import Foundation

/// Identifier for one of the supported case transformations.
///
/// A RawRepresentable enum plus `convert` is the idiomatic Swift equivalent
/// of the TypeScript `caseTransforms` object + `CaseId` union: exhaustive,
/// statically dispatched, and impossible to get out of sync at runtime.
/// `allCases` iterates in declaration order — the same order as the TS keys.
enum CaseId: String, CaseIterable {
    case upper, lower, title, sentence, camel, pascal, snake, kebab, constant, slug
}

/// JavaScript's \w = [A-Za-z0-9_].
private func isWordChar(_ c: Character) -> Bool {
    c.isASCII && (c.isLetter || c.isNumber || c == "_")
}

/// An ASCII letter or digit — the token alphabet of the reference.
private func isAsciiAlnum(_ c: Character) -> Bool {
    c.isASCII && (c.isLetter || c.isNumber)
}

/// Close the current token, if any, pushing it into the output.
private func flush(_ current: inout String, _ out: inout [String]) {
    if !current.isEmpty {
        out.append(current)
        current = ""
    }
}

/// Tokenize `s` into word units.
///
/// Mirrors the TypeScript reference exactly, as one fused forward pass:
///  1. Break at every camelCase boundary (a lowercase letter or digit
///     immediately followed by an uppercase letter).
///  2. Break on runs of whitespace / underscore / hyphen / dot.
///  3. Strip any remaining non-alphanumeric characters and drop empty tokens.
func words(_ s: String) -> [String] {
    let chars = Array(s)
    var out: [String] = []
    var current = ""

    for (i, c) in chars.enumerated() {
        // 1. camelCase boundary: the previous char was lowercase-or-digit and
        //    this one is uppercase — close the current token.
        if i > 0 {
            let prev = chars[i - 1]
            let prevIsLowerOrDigit = prev.isASCII && (prev.isLowercase || prev.isNumber)
            let cIsUpper = c.isASCII && c.isUppercase
            if prevIsLowerOrDigit && cIsUpper {
                flush(&current, &out)
            }
        }

        if c.isWhitespace || c == "_" || c == "-" || c == "." {
            // 2. delimiter — end the current token.
            flush(&current, &out)
        } else if isAsciiAlnum(c) {
            // 3a. keep ASCII letters and digits.
            current.append(c)
        }
        // 3b. anything else (punctuation, symbols) is silently dropped,
        //     matching the reference's `[^a-zA-Z0-9]` stripping.
    }
    flush(&current, &out)
    return out
}

/// Upper-case every character.
func upper(_ s: String) -> String { s.uppercased() }

/// Lower-case every character.
func lower(_ s: String) -> String { s.lowercased() }

/// Capitalize the first character and lowercase the rest.
func cap(_ s: String) -> String {
    guard let first = s.first else { return "" }
    return String(first).uppercased() + s.dropFirst().lowercased()
}

/// Title case: capitalize the first character of each maximal word-run.
///
/// A "word-run" starts at an ASCII word character ([A-Za-z0-9_]) and runs to
/// the next whitespace, mirroring JavaScript's /\w\S*/g. Characters between
/// runs (spacing, punctuation) are preserved verbatim — only each run's first
/// character is touched.
func title(_ s: String) -> String {
    let chars = Array(s)
    var out = ""
    var i = 0
    while i < chars.count {
        if isWordChar(chars[i]) {
            // Consume the whole `\w\S*` run up to the next whitespace.
            let start = i
            i += 1
            while i < chars.count && !chars[i].isWhitespace {
                i += 1
            }
            out += cap(String(chars[start..<i]))
        } else {
            // Not the start of a word-run: copy verbatim.
            out.append(chars[i])
            i += 1
        }
    }
    return out
}

/// Sentence case: lowercase + trim the whole string, then uppercase the first
/// character. Empty (or all-whitespace) input yields empty output.
func sentence(_ s: String) -> String {
    let t = s.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
    guard let first = t.first else { return "" }
    return String(first).uppercased() + t.dropFirst()
}

/// camelCase: the first word stays lowercase, every later word is capitalized.
func camel(_ s: String) -> String {
    var out = ""
    for (i, w) in words(s).enumerated() {
        out += i == 0 ? w.lowercased() : cap(w)
    }
    return out
}

/// PascalCase: every word capitalized and concatenated.
func pascal(_ s: String) -> String {
    var out = ""
    for w in words(s) {
        out += cap(w)
    }
    return out
}

func snake(_ s: String) -> String { words(s).joined(separator: "_").lowercased() }

func kebab(_ s: String) -> String { words(s).joined(separator: "-").lowercased() }

func constant(_ s: String) -> String { words(s).joined(separator: "_").uppercased() }

func slug(_ s: String) -> String { words(s).joined(separator: "-").lowercased() }

/// Apply the case transform identified by `id`.
///
/// String-keyed dispatch is available via `tryConvert`:
/// `CaseId(rawValue: name).map { convert($0, s) }`.
func convert(_ id: CaseId, _ s: String) -> String {
    switch id {
    case .upper: return upper(s)
    case .lower: return lower(s)
    case .title: return title(s)
    case .sentence: return sentence(s)
    case .camel: return camel(s)
    case .pascal: return pascal(s)
    case .snake: return snake(s)
    case .kebab: return kebab(s)
    case .constant: return constant(s)
    case .slug: return slug(s)
    }
}

/// Like `convert` but keyed by the raw string form; nil for an unknown case id.
func tryConvert(_ caseId: String, _ s: String) -> String? {
    guard let id = CaseId(rawValue: caseId) else { return nil }
    return convert(id, s)
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →