Skip to content

ASCII Table — C source

A searchable, filterable reference for all 128 ASCII code points. See the decimal, hex, octal, and binary form of every character, narrow to printable characters only, clamp the code-point range, and click any row to copy. Runs 100% client-side.

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

/*
 * ASCII Table — pure logic (C port): the 128-entry ASCII reference table with
 * search and range filters.
 *
 * Language: C11 — standard library only (stdio / string / ctype / stdbool).
 * Source: CosmoDev polyglot showcase port; canonical = src/lib/ascii-table.ts
 *         + this tool's python.py / rust.rs.
 * License: display source — part of CosmoDev's polyglot tool pages.
 *
 * Each of the 128 ASCII code points is described by its decimal / hex / octal /
 * binary forms, a display glyph, a human name, whether it is printable, whether
 * it is a control character, and (for control chars that have one) a C-style
 * escape sequence.
 */

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

#define ASCII_TABLE_SIZE 128

/*
 * A single ASCII code point and its derived representations.
 * All strings are fixed-size buffers (C has no dynamic strings): every field
 * is sized for its longest value plus the NUL terminator.
 */
typedef struct {
    int dec;            /* Decimal code point, 0–127. */
    char hex[6];        /* Uppercase hex, e.g. "0x41". */
    char oct[5];        /* Zero-padded 3-digit octal, e.g. "101". */
    char binary[10];    /* Zero-padded 8-bit binary, e.g. "01000001". */
    char char_glyph[5]; /* Display glyph (named `char` in the canonical sources); UTF-8 (≤ 3 bytes) + NUL. */
    char name[40];      /* Human-readable name, e.g. "Line Feed (LF)". */
    bool printable;     /* True for 32–126 (visible or space). */
    bool control;       /* True for 0–31 and 127. */
    char escape[3];     /* C-style escape token (e.g. "\\n"), or "" when there is none — C's stand-in for Option. */
} ascii_entry_t;

/*
 * Official ASCII control-character names for 0–31 and 127.
 * Expressed as a switch over every control code point; the default arm is a
 * defensive guard that documents the precondition (the Rust port uses
 * unreachable!() for the same case — C's nearest safe equivalent is "").
 */
static const char *control_name(int code)
{
    switch (code) {
    case 0: return "Null (NUL)";
    case 1: return "Start of Heading (SOH)";
    case 2: return "Start of Text (STX)";
    case 3: return "End of Text (ETX)";
    case 4: return "End of Transmission (EOT)";
    case 5: return "Enquiry (ENQ)";
    case 6: return "Acknowledge (ACK)";
    case 7: return "Bell (BEL)";
    case 8: return "Backspace (BS)";
    case 9: return "Horizontal Tab (HT)";
    case 10: return "Line Feed (LF)";
    case 11: return "Vertical Tab (VT)";
    case 12: return "Form Feed (FF)";
    case 13: return "Carriage Return (CR)";
    case 14: return "Shift Out (SO)";
    case 15: return "Shift In (SI)";
    case 16: return "Data Link Escape (DLE)";
    case 17: return "Device Control 1 (DC1)";
    case 18: return "Device Control 2 (DC2)";
    case 19: return "Device Control 3 (DC3)";
    case 20: return "Device Control 4 (DC4)";
    case 21: return "Negative Acknowledge (NAK)";
    case 22: return "Synchronous Idle (SYN)";
    case 23: return "End of Transmission Block (ETB)";
    case 24: return "Cancel (CAN)";
    case 25: return "End of Medium (EM)";
    case 26: return "Substitute (SUB)";
    case 27: return "Escape (ESC)";
    case 28: return "File Separator (FS)";
    case 29: return "Group Separator (GS)";
    case 30: return "Record Separator (RS)";
    case 31: return "Unit Separator (US)";
    case 127: return "Delete (DEL)";
    default: return ""; /* unreachable with control codes; defensive guard */
    }
}

/*
 * C-style escape for the control chars that own a standard/common one.
 * Returns the literal 2-character source token (backslash + letter), or NULL.
 */
static const char *control_escape(int code)
{
    switch (code) {
    case 0: return "\\0";
    case 7: return "\\a";
    case 8: return "\\b";
    case 9: return "\\t";
    case 10: return "\\n";
    case 11: return "\\v";
    case 12: return "\\f";
    case 13: return "\\r";
    case 27: return "\\e";
    default: return NULL;
    }
}

/*
 * Name for a printable punctuation / symbol glyph, or NULL.
 * Letters and digits are derived in letter_name().
 */
static const char *symbol_name(int code)
{
    switch (code) {
    case 32: return "Space";
    case 33: return "Exclamation mark";
    case 34: return "Quotation mark";
    case 35: return "Number sign";
    case 36: return "Dollar sign";
    case 37: return "Percent sign";
    case 38: return "Ampersand";
    case 39: return "Apostrophe";
    case 40: return "Left parenthesis";
    case 41: return "Right parenthesis";
    case 42: return "Asterisk";
    case 43: return "Plus sign";
    case 44: return "Comma";
    case 45: return "Hyphen / Minus";
    case 46: return "Full stop";
    case 47: return "Slash";
    case 58: return "Colon";
    case 59: return "Semicolon";
    case 60: return "Less-than sign";
    case 61: return "Equals sign";
    case 62: return "Greater-than sign";
    case 63: return "Question mark";
    case 64: return "At sign";
    case 91: return "Left bracket";
    case 92: return "Backslash";
    case 93: return "Right bracket";
    case 94: return "Circumflex / Caret";
    case 95: return "Underscore";
    case 96: return "Grave accent";
    case 123: return "Left brace";
    case 124: return "Vertical bar";
    case 125: return "Right brace";
    case 126: return "Tilde";
    default: return NULL;
    }
}

/* Derive a name for a printable letter or digit. */
static void letter_name(int code, char out[40])
{
    if (code >= 48 && code <= 57)       /* '0'..'9' */
        snprintf(out, 40, "Digit %d", code - 48);
    else if (code >= 65 && code <= 90)  /* 'A'..'Z' */
        snprintf(out, 40, "Uppercase %c", (char)code);
    else if (code >= 97 && code <= 122) /* 'a'..'z' */
        snprintf(out, 40, "Lowercase %c", (char)code);
    else
        snprintf(out, 40, "%c", (char)code);
}

/*
 * Encode a Unicode scalar (U+0800..U+FFFF) as a 3-byte UTF-8 sequence + NUL.
 * Every control-picture glyph used here (U+2400..U+2421) is in that range.
 */
static void utf8_encode3(unsigned cp, char out[4])
{
    out[0] = (char)(0xE0u | (cp >> 12));
    out[1] = (char)(0x80u | ((cp >> 6) & 0x3Fu));
    out[2] = (char)(0x80u | (cp & 0x3Fu));
    out[3] = '\0';
}

/* Zero-padded 8-digit binary (standard C11 has no "%b" conversion). */
static void fmt_binary(unsigned v, char out[10])
{
    for (int i = 0; i < 8; i++)
        out[i] = (char)('0' + ((v >> (7 - i)) & 1u));
    out[8] = '\0';
}

/* Build a single entry from its code point (0–127). */
static ascii_entry_t make_entry(int code)
{
    ascii_entry_t e;
    const bool control = code <= 31 || code == 127;

    e.dec = code;
    snprintf(e.hex, sizeof e.hex, "0x%02X", code);
    snprintf(e.oct, sizeof e.oct, "%03o", code);
    fmt_binary((unsigned)code, e.binary);

    /* Control-picture glyph: U+2400 for 0–31, U+2421 ("symbol for delete") for DEL. */
    if (control) {
        utf8_encode3(code == 127 ? 0x2421u : 0x2400u + (unsigned)code, e.char_glyph);
    } else {
        e.char_glyph[0] = (char)code;
        e.char_glyph[1] = '\0';
    }

    if (control) {
        const char *esc = control_escape(code);
        snprintf(e.name, sizeof e.name, "%s", control_name(code));
        snprintf(e.escape, sizeof e.escape, "%s", esc != NULL ? esc : "");
    } else {
        const char *sym = symbol_name(code);
        if (sym != NULL)
            snprintf(e.name, sizeof e.name, "%s", sym);
        else
            letter_name(code, e.name);
        e.escape[0] = '\0';
    }

    e.printable = !control;
    e.control = control;
    return e;
}

/*
 * The 128-entry table is built once on first use and never mutated afterwards.
 * C11 has no portable call-once without <threads.h>; the first call is assumed
 * single-threaded, and concurrent READS of the finished table are safe.
 */
static ascii_entry_t g_table[ASCII_TABLE_SIZE];
static bool g_ready = false;

static void ascii_init(void)
{
    if (g_ready)
        return;
    for (int i = 0; i < ASCII_TABLE_SIZE; i++)
        g_table[i] = make_entry(i);
    g_ready = true;
}

/* The full 128-entry ASCII table, indexed by code point. */
const ascii_entry_t *ascii_entries(void)
{
    ascii_init();
    return g_table;
}

/*
 * Look up a single entry by code point.
 * Returns NULL for out-of-range input — the idiomatic C equivalent of the
 * TypeScript variant's `null` and Rust's `Option`.
 */
const ascii_entry_t *ascii_get_entry(int code)
{
    if (code < 0 || code > 127)
        return NULL;
    return &ascii_entries()[code];
}

static char lower_byte(char c)
{
    return (char)tolower((unsigned char)c);
}

/*
 * Case-insensitive substring test with explicit lengths: every haystack and
 * needle byte is ASCII-lowered before comparison, which matches the reference
 * implementations (they lowercase both sides).
 */
static bool ci_contains_n(const char *hay, size_t hlen, const char *needle, size_t nlen)
{
    if (nlen == 0)
        return true;
    if (hlen < nlen)
        return false;
    for (size_t i = 0; i + nlen <= hlen; i++) {
        size_t j = 0;
        while (j < nlen && lower_byte(hay[i + j]) == lower_byte(needle[j]))
            j++;
        if (j == nlen)
            return true;
    }
    return false;
}

/*
 * Filter the table.
 *
 * `query` is a case-insensitive substring matched against dec / hex / oct /
 * binary / name / char / escape; surrounding whitespace is trimmed.
 * `printable_only` hides controls. `min` / `max` clamp the decimal range
 * (defaults 0–127). Matching entries are stored (at most `cap` pointers) into
 * `out` — pass NULL to just count. The number of matches is returned; when it
 * exceeds `cap`, the excess entries are counted but not stored.
 */
size_t ascii_filter_table(const char *query, bool printable_only, int min, int max,
                          const ascii_entry_t *out[], size_t cap)
{
    /* Trim surrounding whitespace; the needle length is tracked explicitly. */
    const char *q = query != NULL ? query : "";
    while (*q != '\0' && isspace((unsigned char)*q))
        q++;
    size_t qlen = strlen(q);
    while (qlen > 0 && isspace((unsigned char)q[qlen - 1]))
        qlen--;

    size_t count = 0;
    for (int i = 0; i < ASCII_TABLE_SIZE; i++) {
        const ascii_entry_t *e = &ascii_entries()[i];

        if (printable_only && !e->printable)
            continue;
        if (e->dec < min || e->dec > max)
            continue;

        if (qlen > 0) {
            char dec[4];
            snprintf(dec, sizeof dec, "%d", e->dec);
            bool hit = ci_contains_n(dec, strlen(dec), q, qlen)
                    || ci_contains_n(e->hex, strlen(e->hex), q, qlen)
                    || ci_contains_n(e->oct, strlen(e->oct), q, qlen)
                    || ci_contains_n(e->binary, strlen(e->binary), q, qlen)
                    || ci_contains_n(e->name, strlen(e->name), q, qlen)
                    || ci_contains_n(e->char_glyph, strlen(e->char_glyph), q, qlen)
                    || ci_contains_n(e->escape, strlen(e->escape), q, qlen);
            if (!hit)
                continue;
        }

        if (out != NULL && count < cap)
            out[count] = e;
        count++;
    }
    return count;
}

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 →