Skip to content

Case Converter — Kotlin 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 Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.

// case-converter — pure, deterministic text-case transformations.
//
// Language: Kotlin 1.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. Case mapping is Unicode-aware and locale-invariant
// (String.uppercase() / lowercase()), matching JavaScript.

// --- Compiled patterns -------------------------------------------------------
// Each mirrors a regex from the TypeScript reference. The inline (?U) flag
// widens \s / \S to Unicode whitespace (java.util.regex's
// UNICODE_CHARACTER_CLASS), matching the Python and Rust ports; the explicit
// [a-z0-9] / [A-Za-z0-9_] classes stay ASCII exactly like JavaScript's.

// A camelCase seam: a lowercase letter or digit immediately followed by an
// uppercase letter. We splice a space in here before splitting.
private val CAMEL_BOUNDARY = Regex("([a-z0-9])([A-Z])")

// Word delimiters: any run of whitespace, underscore, hyphen, or dot.
private val SEPARATORS = Regex("(?U)[\\s_\\-.]+")

// Anything that is not an ASCII letter or digit — stripped from each token.
private val NON_ALNUM = Regex("[^a-zA-Z0-9]")

// A title "word": a maximal run that starts with an ASCII word character and
// continues with non-whitespace (JavaScript's /\w\S*/g, \w spelled out).
private val TITLE_WORD = Regex("(?U)[A-Za-z0-9_]\\S*")

/**
 * Tokenize [s] into word units.
 *
 * The pipeline reproduces the TypeScript reference step for step:
 *  1. Insert a space at every camelCase boundary.
 *  2. Split on runs of whitespace / underscore / hyphen / dot.
 *  3. Strip remaining non-alphanumerics and drop empty tokens.
 */
fun words(s: String): List<String> =
    SEPARATORS.split(CAMEL_BOUNDARY.replace(s, "$1 $2"))
        .map { NON_ALNUM.replace(it, "") }
        .filter { it.isNotEmpty() } // filter(Boolean) in the TS source

/** Capitalize the first character and lowercase the rest. */
fun cap(s: String): String =
    if (s.isEmpty()) s else s[0].uppercaseChar().toString() + s.substring(1).lowercase()

/** Upper-case every character. */
fun upper(s: String): String = s.uppercase()

/** Lower-case every character. */
fun lower(s: String): String = s.lowercase()

/**
 * Title case: capitalize the first character of each maximal word-run.
 * Characters between runs (spacing, punctuation) are left untouched.
 */
fun title(s: String): String = TITLE_WORD.replace(s) { cap(it.value) }

/**
 * Sentence case: lowercase + trim the whole string, then uppercase only the
 * first character. Empty input yields empty output.
 */
fun sentence(s: String): String {
    val t = s.lowercase().trim()
    return if (t.isEmpty()) t else t[0].uppercaseChar().toString() + t.substring(1)
}

/** camelCase: the first word stays lowercase, every later word is capitalized. */
fun camel(s: String): String =
    words(s).mapIndexed { i, w -> if (i == 0) w.lowercase() else cap(w) }.joinToString("")

/** PascalCase: every word capitalized and concatenated. */
fun pascal(s: String): String = words(s).joinToString("") { cap(it) }

/** snake_case — "_".join(words).lower() in the reference. */
fun snake(s: String): String = words(s).joinToString("_").lowercase()

/** kebab-case — "-".join(words).lower() in the reference. */
fun kebab(s: String): String = words(s).joinToString("-").lowercase()

/** CONSTANT_CASE — "_".join(words).upper() in the reference. */
fun constant(s: String): String = words(s).joinToString("_").uppercase()

/** slug (kebab's URL-safe twin) — "-".join(words).lower() in the reference. */
fun slug(s: String): String = words(s).joinToString("-").lowercase()

/**
 * The case-transform registry: CaseId -> pure function. Keys mirror the
 * TypeScript `caseTransforms` object 1:1; LinkedHashMap preserves insertion
 * order and the map is read-only.
 */
val caseTransforms: Map<String, (String) -> String> = linkedMapOf(
    "upper" to ::upper,
    "lower" to ::lower,
    "title" to ::title,
    "sentence" to ::sentence,
    "camel" to ::camel,
    "pascal" to ::pascal,
    "snake" to ::snake,
    "kebab" to ::kebab,
    "constant" to ::constant,
    "slug" to ::slug,
)

/** Valid case identifiers, in registry order. */
val caseIds: List<String> = caseTransforms.keys.toList()

/**
 * Apply a named case transform.
 *
 * @throws IllegalArgumentException if [caseId] is not a known case
 */
fun convert(caseId: String, s: String): String =
    caseTransforms[caseId]?.invoke(s)
        ?: throw IllegalArgumentException("Unknown case: $caseId")

/** Like [convert] but returns null for an unknown case id. */
fun tryConvert(caseId: String, s: String): String? = caseTransforms[caseId]?.invoke(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 →