Skip to content

ASCII Table — Rust 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 Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! ASCII Table — pure logic (Rust port).
//!
//! CosmoDev polyglot showcase port of the `ascii-table` tool, ported from
//! `src/lib/ascii-table.ts`. Every field is a pure function of the code point
//! 0–127; no I/O, no external crates — the Rust standard library only.
//!
//! 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.

use std::sync::OnceLock;

/// A single ASCII code point and its derived representations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
    /// Decimal code point, 0–127.
    pub dec: u8,
    /// Uppercase hex, e.g. `"0x41"`.
    pub hex: String,
    /// Zero-padded 3-digit octal, e.g. `"101"`.
    pub oct: String,
    /// Zero-padded 8-bit binary, e.g. `"01000001"`.
    pub binary: String,
    /// Display glyph: the literal char for printables, a control-picture glyph otherwise.
    pub char: String,
    /// Human-readable name, e.g. `"Line Feed (LF)"`, `"Uppercase A"`, `"Space"`.
    pub name: String,
    /// True for 32–126 (visible or space).
    pub printable: bool,
    /// True for 0–31 and 127.
    pub control: bool,
    /// C-style escape for control chars that have one (e.g. `"\\n"`); `None` otherwise.
    pub escape: Option<String>,
}

/// Official ASCII control-character names for 0–31 and 127.
///
/// Expressed as a `match` so the compiler verifies exhaustiveness — every
/// control code point is labeled, and adding a new branch without coverage is a
/// compile error. The unreachable arm documents the precondition.
fn control_name(code: u8) -> &'static str {
    match code {
        0 => "Null (NUL)",
        1 => "Start of Heading (SOH)",
        2 => "Start of Text (STX)",
        3 => "End of Text (ETX)",
        4 => "End of Transmission (EOT)",
        5 => "Enquiry (ENQ)",
        6 => "Acknowledge (ACK)",
        7 => "Bell (BEL)",
        8 => "Backspace (BS)",
        9 => "Horizontal Tab (HT)",
        10 => "Line Feed (LF)",
        11 => "Vertical Tab (VT)",
        12 => "Form Feed (FF)",
        13 => "Carriage Return (CR)",
        14 => "Shift Out (SO)",
        15 => "Shift In (SI)",
        16 => "Data Link Escape (DLE)",
        17 => "Device Control 1 (DC1)",
        18 => "Device Control 2 (DC2)",
        19 => "Device Control 3 (DC3)",
        20 => "Device Control 4 (DC4)",
        21 => "Negative Acknowledge (NAK)",
        22 => "Synchronous Idle (SYN)",
        23 => "End of Transmission Block (ETB)",
        24 => "Cancel (CAN)",
        25 => "End of Medium (EM)",
        26 => "Substitute (SUB)",
        27 => "Escape (ESC)",
        28 => "File Separator (FS)",
        29 => "Group Separator (GS)",
        30 => "Record Separator (RS)",
        31 => "Unit Separator (US)",
        127 => "Delete (DEL)",
        // Only called with control codes; defensive guard for misuse.
        _ => unreachable!("control_name called with non-control code point {code}"),
    }
}

/// C-style escape for the control chars that own a standard/common one.
/// Returns the literal 2-character source token (backslash + letter), or `None`.
fn control_escape(code: u8) -> Option<&'static str> {
    Some(match code {
        0 => "\\0",
        7 => "\\a",
        8 => "\\b",
        9 => "\\t",
        10 => "\\n",
        11 => "\\v",
        12 => "\\f",
        13 => "\\r",
        27 => "\\e",
        _ => return None,
    })
}

/// Name for a printable punctuation / symbol glyph, if it has a dedicated one.
/// Letters and digits are derived in [`letter_name`].
fn symbol_name(code: u8) -> Option<&'static str> {
    Some(match code {
        32 => "Space",
        33 => "Exclamation mark",
        34 => "Quotation mark",
        35 => "Number sign",
        36 => "Dollar sign",
        37 => "Percent sign",
        38 => "Ampersand",
        39 => "Apostrophe",
        40 => "Left parenthesis",
        41 => "Right parenthesis",
        42 => "Asterisk",
        43 => "Plus sign",
        44 => "Comma",
        45 => "Hyphen / Minus",
        46 => "Full stop",
        47 => "Slash",
        58 => "Colon",
        59 => "Semicolon",
        60 => "Less-than sign",
        61 => "Equals sign",
        62 => "Greater-than sign",
        63 => "Question mark",
        64 => "At sign",
        91 => "Left bracket",
        92 => "Backslash",
        93 => "Right bracket",
        94 => "Circumflex / Caret",
        95 => "Underscore",
        96 => "Grave accent",
        123 => "Left brace",
        124 => "Vertical bar",
        125 => "Right brace",
        126 => "Tilde",
        _ => return None,
    })
}

/// Derive a name for a printable letter or digit.
fn letter_name(code: u8) -> String {
    let ch = char::from(code);
    match code {
        48..=57 => format!("Digit {}", code - 48),             // '0'..'9'
        65..=90 => format!("Uppercase {ch}"),                   // 'A'..'Z'
        97..=122 => format!("Lowercase {ch}"),                  // 'a'..'z'
        _ => ch.to_string(),
    }
}

/// Build a single entry from its code point (0–127).
fn make_entry(code: u8) -> Entry {
    let control = code <= 31 || code == 127;

    // Control-picture glyph: U+2400 for 0–31, U+2421 ("symbol for delete") for DEL.
    // char::from_u32 is defensive — these code points are all valid scalar values.
    let char_glyph = if control {
        let cp = if code == 127 { 0x2421 } else { 0x2400 + code as u32 };
        char::from_u32(cp).map(char::to_string).unwrap_or_default()
    } else {
        char::from(code).to_string()
    };

    let name = if control {
        control_name(code).to_string()
    } else if let Some(sym) = symbol_name(code) {
        sym.to_string()
    } else {
        letter_name(code)
    };

    let escape = if control {
        control_escape(code).map(str::to_string)
    } else {
        None
    };

    Entry {
        dec: code,
        hex: format!("0x{:02X}", code),
        oct: format!("{:03o}", code),
        binary: format!("{:08b}", code),
        char: char_glyph,
        name,
        printable: !control,
        control,
        escape,
    }
}

// The 128-entry table is built once and cached for the lifetime of the program.
// `OnceLock` (std::sync) gives us a lazily-initialized static without any
// external crate like lazy_static / once_cell.
static ENTRIES: OnceLock<Vec<Entry>> = OnceLock::new();

/// The full 128-entry ASCII table, indexed by code point.
pub fn entries() -> &'static [Entry] {
    ENTRIES.get_or_init(|| (0..=127).map(make_entry).collect())
}

/// Look up a single entry by code point.
///
/// Returns `None` for out-of-range input. The TypeScript variant returns `null`;
/// Rust's idiomatic equivalent is `Option`, which the type system forces callers
/// to handle. The borrow ties the returned reference to the static table.
pub fn get_entry(code: i32) -> Option<&'static Entry> {
    if !(0..=127).contains(&code) {
        return None;
    }
    Some(&entries()[code as usize])
}

/// Optional parameters for [`filter_table`]. All fields optional.
#[derive(Default)]
pub struct FilterOpts {
    /// Case-insensitive substring matched against dec / hex / oct / binary / name / char / escape.
    pub query: Option<String>,
    /// Hide control characters.
    pub printable_only: bool,
    /// Lower decimal bound (default 0).
    pub min: Option<u8>,
    /// Upper decimal bound (default 127).
    pub max: Option<u8>,
}

/// Filter the table.
///
/// `query` is a case-insensitive substring matched against dec / hex / oct /
/// binary / name / char / escape. `printable_only` hides controls. `min` / `max`
/// clamp the decimal range (defaults 0–127). Returns references into the shared
/// static table, so filtering is allocation-free apart from the result `Vec`.
pub fn filter_table(opts: &FilterOpts) -> Vec<&'static Entry> {
    let q = opts.query.as_deref().unwrap_or("").trim().to_lowercase();
    let lo = opts.min.unwrap_or(0);
    let hi = opts.max.unwrap_or(127);

    entries()
        .iter()
        .filter(|e| {
            if opts.printable_only && !e.printable {
                return false;
            }
            if e.dec < lo || e.dec > hi {
                return false;
            }
            if q.is_empty() {
                return true;
            }
            // Lowercase the fields that carry case; glyphs and numerals pass through.
            let dec = e.dec.to_string();
            let hex_lc = e.hex.to_lowercase();
            let name_lc = e.name.to_lowercase();
            let char_lc = e.char.to_lowercase();
            let esc = e.escape.as_deref().unwrap_or("");
            [dec.as_str(), hex_lc.as_str(), &e.oct, &e.binary, name_lc.as_str(), char_lc.as_str(), esc]
                .into_iter()
                .any(|h| h.contains(&q))
        })
        .collect()
}

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 →