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 (C11, 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.
 *
 * The C standard library has no Unicode case tables (towupper is
 * locale-dependent), so this port maps ASCII letters and passes every other
 * byte through verbatim — the tool's token alphabet is [a-zA-Z0-9] anyway.
 * The single-byte scans are safe for UTF-8 payload because every ASCII test
 * below only ever matches single-byte characters; multi-byte sequences ride
 * along untouched inside (or between) tokens.
 */

#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

/* --- Character classification (mirrors the JS regexes' ASCII classes) ---- */

static bool is_ascii_lower(unsigned char c) { return c >= 'a' && c <= 'z'; }
static bool is_ascii_upper(unsigned char c) { return c >= 'A' && c <= 'Z'; }
static bool is_ascii_digit(unsigned char c) { return c >= '0' && c <= '9'; }
static bool is_ascii_alnum(unsigned char c)
{
    return is_ascii_lower(c) || is_ascii_upper(c) || is_ascii_digit(c);
}
/* JavaScript's \w = [A-Za-z0-9_]. */
static bool is_word_char(unsigned char c) { return is_ascii_alnum(c) || c == '_'; }

/* --- Growable byte buffer -------------------------------------------------- */

typedef struct {
    char *data;
    size_t len;
    size_t cap;
} strbuf;

static bool strbuf_reserve(strbuf *b, size_t extra)
{
    size_t need = b->len + extra + 1; /* room for a NUL terminator */
    if (need <= b->cap)
        return true;
    size_t cap = b->cap ? b->cap : 16;
    while (cap < need)
        cap *= 2;
    char *p = realloc(b->data, cap);
    if (p == NULL)
        return false;
    b->data = p;
    b->cap = cap;
    return true;
}

static bool strbuf_push(strbuf *b, char c)
{
    if (!strbuf_reserve(b, 1))
        return false;
    b->data[b->len++] = c;
    return true;
}

static bool strbuf_append(strbuf *b, const char *s, size_t n)
{
    if (!strbuf_reserve(b, n))
        return false;
    memcpy(b->data + b->len, s, n);
    b->len += n;
    return true;
}

/* Hand the buffer's NUL-terminated contents to the caller (NULL on OOM). */
static char *strbuf_take(strbuf *b)
{
    if (!strbuf_push(b, '\0'))
        return NULL;
    char *out = b->data;
    b->data = NULL;
    b->len = 0;
    b->cap = 0;
    return out;
}

/* --- Word list -------------------------------------------------------------- */

typedef struct {
    char **items; /* each element is an owned, malloc'd NUL-terminated string */
    size_t len;
} word_list;

static bool word_list_push(word_list *wl, char *owned)
{
    char **p = realloc(wl->items, (wl->len + 1) * sizeof *p);
    if (p == NULL) {
        free(owned);
        return false;
    }
    wl->items = p;
    wl->items[wl->len++] = owned;
    return true;
}

/* Release a list returned by words() — public because words() hands the
 * caller ownership of the list and every token in it. */
void word_list_free(word_list *wl)
{
    for (size_t i = 0; i < wl->len; i++)
        free(wl->items[i]);
    free(wl->items);
    wl->items = NULL;
    wl->len = 0;
}

/* Close the current token, if any, pushing it into the output list. */
static void flush_token(strbuf *cur, word_list *out)
{
    if (cur->len == 0)
        return;
    char *tok = strbuf_take(cur);
    if (tok != NULL)
        word_list_push(out, tok);
}

/*
 * Tokenize `s` into word units — one forward pass fusing the three steps of
 * the TypeScript reference:
 *  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. Keep ASCII letters and digits; drop everything else and empty tokens.
 *
 * Returns a list whose caller becomes the owner; release with
 * word_list_free(). On allocation failure `items` is NULL and `len` is 0.
 */
word_list words(const char *s)
{
    word_list out = {NULL, 0};
    strbuf cur = {NULL, 0, 0};
    size_t n = strlen(s);

    for (size_t i = 0; i < n; i++) {
        unsigned char c = (unsigned char)s[i];

        /* 1. camelCase boundary: the previous char was lowercase-or-digit
         *    and this one is uppercase — close the current token. */
        if (i > 0) {
            unsigned char prev = (unsigned char)s[i - 1];
            if ((is_ascii_lower(prev) || is_ascii_digit(prev)) && is_ascii_upper(c))
                flush_token(&cur, &out);
        }

        if (isspace(c) || c == '_' || c == '-' || c == '.') {
            /* 2. delimiter — end the current token. */
            flush_token(&cur, &out);
        } else if (is_ascii_alnum(c)) {
            /* 3a. keep ASCII letters and digits. */
            if (!strbuf_push(&cur, (char)c)) {
                word_list_free(&out);
                free(cur.data);
                return (word_list){NULL, 0};
            }
        }
        /* 3b. anything else (punctuation, symbols) is silently dropped,
         *     matching the reference's `[^a-zA-Z0-9]` stripping. */
    }
    flush_token(&cur, &out);
    free(cur.data);
    return out;
}

/* --- Case mapping (ASCII; other bytes pass through verbatim) --------------- */

/* Map ASCII letters through `map`; copy every other byte unchanged. */
static char *map_ascii(const char *s, int (*map)(int))
{
    size_t n = strlen(s);
    char *out = malloc(n + 1);
    if (out == NULL)
        return NULL;
    for (size_t i = 0; i < n; i++)
        out[i] = (char)map((unsigned char)s[i]);
    out[n] = '\0';
    return out;
}

/* Upper-case every ASCII letter. */
char *upper(const char *s) { return map_ascii(s, toupper); }

/* Lower-case every ASCII letter. */
char *lower(const char *s) { return map_ascii(s, tolower); }

/* Capitalize the first character and lowercase the rest. */
char *cap(const char *s)
{
    size_t n = strlen(s);
    char *out = malloc(n + 1);
    if (out == NULL)
        return NULL;
    if (n > 0) {
        out[0] = (char)toupper((unsigned char)s[0]);
        for (size_t i = 1; i < n; i++)
            out[i] = (char)tolower((unsigned char)s[i]);
    }
    out[n] = '\0';
    return out;
}

/*
 * 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 the JavaScript \w\S* word-run regex.
 * Characters between
 * runs (spacing, punctuation) are preserved verbatim — only each run's first
 * character is touched.
 */
char *title(const char *s)
{
    size_t n = strlen(s);
    char *out = malloc(n + 1);
    if (out == NULL)
        return NULL;

    size_t i = 0, o = 0;
    while (i < n) {
        unsigned char c = (unsigned char)s[i];
        if (is_word_char(c)) {
            /* Consume the whole `\w\S*` run up to the next whitespace. */
            size_t start = i;
            i++;
            while (i < n && !isspace((unsigned char)s[i]))
                i++;
            out[o++] = (char)toupper((unsigned char)s[start]);
            for (size_t k = start + 1; k < i; k++)
                out[o++] = (char)tolower((unsigned char)s[k]);
        } else {
            /* Not the start of a word-run: copy verbatim. */
            out[o++] = (char)c;
            i++;
        }
    }
    out[o] = '\0';
    return out;
}

/*
 * Sentence case: lowercase + trim the whole string, then uppercase the first
 * character. Empty (or all-whitespace) input yields empty output.
 */
char *sentence(const char *s)
{
    char *t = map_ascii(s, tolower);
    if (t == NULL)
        return NULL;

    size_t n = strlen(t);
    size_t a = 0, b = n; /* [a, b) is the trimmed span */
    while (a < b && isspace((unsigned char)t[a]))
        a++;
    while (b > a && isspace((unsigned char)t[b - 1]))
        b--;
    if (b > a)
        t[a] = (char)toupper((unsigned char)t[a]);
    memmove(t, t + a, b - a);
    t[b - a] = '\0';
    return t;
}

/* --- Token-based transforms -------------------------------------------------- */

/* Join the tokens with `sep` into one owned string (NULL on OOM). */
static char *join_words(const word_list *wl, char sep)
{
    strbuf b = {NULL, 0, 0};
    for (size_t i = 0; i < wl->len; i++) {
        if (i > 0 && !strbuf_push(&b, sep)) {
            free(b.data);
            return NULL;
        }
        if (!strbuf_append(&b, wl->items[i], strlen(wl->items[i]))) {
            free(b.data);
            return NULL;
        }
    }
    return strbuf_take(&b);
}

/* camelCase: the first word stays lowercase, every later word is capitalized. */
char *camel(const char *s)
{
    word_list w = words(s);
    strbuf b = {NULL, 0, 0};
    for (size_t i = 0; i < w.len; i++) {
        char *piece = (i == 0) ? map_ascii(w.items[i], tolower) : cap(w.items[i]);
        if (piece == NULL || !strbuf_append(&b, piece, strlen(piece))) {
            free(piece);
            word_list_free(&w);
            free(b.data);
            return NULL;
        }
        free(piece);
    }
    word_list_free(&w);
    return strbuf_take(&b);
}

/* PascalCase: every word capitalized and concatenated. */
char *pascal(const char *s)
{
    word_list w = words(s);
    strbuf b = {NULL, 0, 0};
    for (size_t i = 0; i < w.len; i++) {
        char *piece = cap(w.items[i]);
        if (piece == NULL || !strbuf_append(&b, piece, strlen(piece))) {
            free(piece);
            word_list_free(&w);
            free(b.data);
            return NULL;
        }
        free(piece);
    }
    word_list_free(&w);
    return strbuf_take(&b);
}

/* Join + re-case helpers: `snake`/`kebab`/`constant`/`slug` all join the
 * tokens first, then map the joined result — exactly like the reference's
 * "_".join(words).lower() chains. */
static char *joined_case(const char *s, char sep, int (*map)(int))
{
    word_list w = words(s);
    char *joined = join_words(&w, sep);
    word_list_free(&w);
    if (joined == NULL)
        return NULL;
    char *out = map_ascii(joined, map);
    free(joined);
    return out;
}

char *snake(const char *s) { return joined_case(s, '_', tolower); }
char *kebab(const char *s) { return joined_case(s, '-', tolower); }
char *constant(const char *s) { return joined_case(s, '_', toupper); }
char *slug(const char *s) { return joined_case(s, '-', tolower); }

/* --- Case-id dispatch -------------------------------------------------------- */

/* Identifier for one of the supported case transformations — the C
 * equivalent of the TypeScript `CaseId` union + caseTransforms keys. */
typedef enum {
    CASE_UPPER,
    CASE_LOWER,
    CASE_TITLE,
    CASE_SENTENCE,
    CASE_CAMEL,
    CASE_PASCAL,
    CASE_SNAKE,
    CASE_KEBAB,
    CASE_CONSTANT,
    CASE_SLUG
} case_id;

/* The registry's key list, in the same order as the TS object. */
static const char *const CASE_ID_NAMES[] = {
    "upper", "lower", "title", "sentence", "camel",
    "pascal", "snake", "kebab", "constant", "slug",
};

#define CASE_ID_COUNT (sizeof CASE_ID_NAMES / sizeof CASE_ID_NAMES[0])

/* Lowercase string form, matching the TypeScript object keys. */
const char *case_id_name(case_id id)
{
    if ((size_t)id < CASE_ID_COUNT)
        return CASE_ID_NAMES[(size_t)id];
    return NULL;
}

/* Parse a case id from its lowercase string form. */
bool case_id_from_name(const char *name, case_id *out)
{
    for (size_t i = 0; i < CASE_ID_COUNT; i++) {
        if (strcmp(name, CASE_ID_NAMES[i]) == 0) {
            *out = (case_id)i;
            return true;
        }
    }
    return false;
}

/* Apply the case transform identified by `id` (caller frees the result). */
char *convert(case_id id, const char *s)
{
    switch (id) {
    case CASE_UPPER:    return upper(s);
    case CASE_LOWER:    return lower(s);
    case CASE_TITLE:    return title(s);
    case CASE_SENTENCE: return sentence(s);
    case CASE_CAMEL:    return camel(s);
    case CASE_PASCAL:   return pascal(s);
    case CASE_SNAKE:    return snake(s);
    case CASE_KEBAB:    return kebab(s);
    case CASE_CONSTANT: return constant(s);
    case CASE_SLUG:     return slug(s);
    }
    return NULL; /* unreachable — every enum value is handled above */
}

/* String-keyed dispatch: NULL when `name` is not a known case id (and on
 * allocation failure). The raising `convert` analogue is
 * case_id_from_name + convert. */
char *try_convert(const char *name, const char *s)
{
    case_id id;
    if (!case_id_from_name(name, &id))
        return NULL;
    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 →