Skip to content

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

// case-converter — pure, deterministic text-case transformations.
//
// Language: C# 12 / .NET 8 (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 culture-invariant
// (ToUpperInvariant / ToLowerInvariant), matching JavaScript's
// toUpperCase / toLowerCase without locale surprises like the Turkish i.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace CosmoDev.CaseConverter;

/// <summary>Pure, deterministic text-case transformations.</summary>
public static class CaseConverter
{
    // --- Compiled patterns ----------------------------------------------------
    // Each mirrors a regex from the TypeScript reference.

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

    // Word delimiters: any run of whitespace (.NET \s is Unicode), underscore,
    // hyphen, or dot.
    private static readonly Regex Separators = new(@"[\s_\-.]+");

    // Anything that is not an ASCII letter or digit — stripped from each token.
    private static readonly Regex NonAlnum = new(@"[^a-zA-Z0-9]");

    // A title "word": a maximal run that starts with an ASCII word character
    // ([A-Za-z0-9_] — spelled out because .NET \w is Unicode-wide, unlike
    // JavaScript's) and continues with non-whitespace.
    private static readonly Regex TitleWord = new(@"[A-Za-z0-9_]\S*");

    /// <summary>
    /// Tokenize <paramref name="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.
    /// </summary>
    public static List<string> Words(string s) =>
        Separators.Split(CamelBoundary.Replace(s, "$1 $2"))
            .Select(part => NonAlnum.Replace(part, ""))
            .Where(cleaned => cleaned.Length > 0)
            .ToList();

    /// <summary>Upper-case every character.</summary>
    public static string Upper(string s) => s.ToUpperInvariant();

    /// <summary>Lower-case every character.</summary>
    public static string Lower(string s) => s.ToLowerInvariant();

    /// <summary>Capitalize the first character and lowercase the rest.</summary>
    public static string Cap(string s) =>
        s.Length == 0 ? s : char.ToUpperInvariant(s[0]) + s[1..].ToLowerInvariant();

    /// <summary>
    /// Title case: capitalize the first character of each maximal word-run.
    /// Characters between runs (spacing, punctuation) are left untouched.
    /// </summary>
    public static string Title(string s) => TitleWord.Replace(s, m => Cap(m.Value));

    /// <summary>
    /// Sentence case: lowercase + trim the whole string, then uppercase only
    /// the first character. Empty input yields empty output.
    /// </summary>
    public static string Sentence(string s)
    {
        string t = s.ToLowerInvariant().Trim();
        return t.Length == 0 ? t : char.ToUpperInvariant(t[0]) + t[1..];
    }

    /// <summary>camelCase: the first word stays lowercase, every later word is capitalized.</summary>
    public static string Camel(string s) => string.Join("",
        Words(s).Select((w, i) => i == 0 ? w.ToLowerInvariant() : Cap(w)));

    /// <summary>PascalCase: every word capitalized and concatenated.</summary>
    public static string Pascal(string s) => string.Join("", Words(s).Select(Cap));

    /// <summary>snake_case — "_".join(words).lower() in the reference.</summary>
    public static string Snake(string s) => string.Join("_", Words(s)).ToLowerInvariant();

    /// <summary>kebab-case — "-".join(words).lower() in the reference.</summary>
    public static string Kebab(string s) => string.Join("-", Words(s)).ToLowerInvariant();

    /// <summary>CONSTANT_CASE — "_".join(words).upper() in the reference.</summary>
    public static string Constant(string s) => string.Join("_", Words(s)).ToUpperInvariant();

    /// <summary>slug (kebab's URL-safe twin) — "-".join(words).lower() in the reference.</summary>
    public static string Slug(string s) => string.Join("-", Words(s)).ToLowerInvariant();

    // The case-transform registry: CaseId -> pure function. Keys mirror the
    // TypeScript `caseTransforms` object 1:1 (insertion order preserved).
    private static readonly Dictionary<string, Func<string, string>> CaseTransforms = new()
    {
        ["upper"] = Upper,
        ["lower"] = Lower,
        ["title"] = Title,
        ["sentence"] = Sentence,
        ["camel"] = Camel,
        ["pascal"] = Pascal,
        ["snake"] = Snake,
        ["kebab"] = Kebab,
        ["constant"] = Constant,
        ["slug"] = Slug,
    };

    /// <summary>Read-only view of the registry (the TS object's public surface).</summary>
    public static IReadOnlyDictionary<string, Func<string, string>> Transforms => CaseTransforms;

    /// <summary>
    /// Apply a named case transform. Throws <see cref="ArgumentException"/>
    /// when <paramref name="caseId"/> is not a known case.
    /// </summary>
    public static string Convert(string caseId, string s) =>
        CaseTransforms.TryGetValue(caseId, out var fn)
            ? fn(s)
            : throw new ArgumentException($"Unknown case: {caseId}", nameof(caseId));

    /// <summary>Like <see cref="Convert"/> but returns null for an unknown case id.</summary>
    public static string? TryConvert(string caseId, string s) =>
        CaseTransforms.TryGetValue(caseId, out var fn) ? fn(s) : null;
}

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 →