Skip to content

Number Base Converter — Rust source

Convert numbers between binary, octal, decimal and hexadecimal. BigInt-powered, so it handles arbitrarily large values without precision loss.

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

// number-base — Arbitrary-precision base conversion.
// Language: Rust.
//
// CosmoDev polyglot showcase port of the `number-base` tool, ported from
// src/lib/numberBase.ts. Display source — part of CosmoDev's polyglot tool pages.
//
// Rust's standard library does not ship a big integer, so this file carries a
// tiny arbitrary-precision core (base-2^32 limbs) just large enough to mirror
// the TypeScript BigInt behavior: exact results for inputs of any length, bases
// 2–36. Standard library only, no external crates — and no crypto, just string
// and integer arithmetic.

/// The full alphabet of digits for bases up to 36. Indexing it by a value in
/// 0..36 yields the lowercase character for that digit.
const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";

/// A non-negative arbitrary-precision integer, stored as little-endian base-2^32
/// limbs. The empty vector represents zero, which keeps `is_zero` an O(1) check.
#[derive(Clone)]
struct Mag(Vec<u32>);

impl Mag {
    fn zero() -> Self {
        Mag(Vec::new())
    }

    fn is_zero(&self) -> bool {
        self.0.is_empty()
    }

    /// Multiply in place by a small value. Every base we ever pass here is ≤ 36,
    /// so the per-limb product plus carry always fits in a u64.
    fn mul_small(&mut self, by: u32) {
        let mut carry: u64 = 0;
        for limb in self.0.iter_mut() {
            let prod = (*limb as u64) * (by as u64) + carry;
            *limb = (prod & 0xFFFF_FFFF) as u32;
            carry = prod >> 32;
        }
        // A leftover carry spills into a fresh most-significant limb.
        if carry != 0 {
            self.0.push(carry as u32);
        }
    }

    /// Add a small value in place, propagating carries until one doesn't occur.
    fn add_small(&mut self, val: u32) {
        let mut carry = val as u64;
        for limb in self.0.iter_mut() {
            let sum = (*limb as u64) + carry;
            *limb = (sum & 0xFFFF_FFFF) as u32;
            carry = sum >> 32;
            if carry == 0 {
                return;
            }
        }
        if carry != 0 {
            self.0.push(carry as u32);
        }
    }

    /// Divide in place by a small value, returning the remainder. We walk
    /// most-significant limb first so the running remainder folds downward into
    /// each lower limb, exactly like long division on paper.
    fn divmod_small(&mut self, divisor: u32) -> u32 {
        let mut remainder: u64 = 0;
        for limb in self.0.iter_mut().rev() {
            let cur = (remainder << 32) | (*limb as u64);
            *limb = (cur / divisor as u64) as u32;
            remainder = cur % divisor as u64;
        }
        // Trim any leading zeros so is_zero stays meaningful and the caller's
        // loop terminates the moment the number reaches zero.
        while self.0.last() == Some(&0) {
            self.0.pop();
        }
        remainder as u32
    }
}

/// Map a byte to its numeric value: 0–9 for '0'–'9', 10–35 for 'a'–'z'. Returns
/// None for any byte that is not a digit in a supported base.
fn digit_value(ch: u8) -> Option<u32> {
    match ch {
        b'0'..=b'9' => Some((ch - b'0') as u32),
        b'a'..=b'z' => Some((ch - b'a') as u32 + 10),
        _ => None,
    }
}

/// Arbitrary-precision signed integer: a magnitude plus a sign flag. This is the
/// value type the parser produces and the formatter consumes, mirroring the
/// `bigint` that flows between `parseBigInt` and `formatBigInt` in the TS source.
pub struct BigInt {
    mag: Mag,
    negative: bool,
}

/// Parse `input` as a base-`base` integer (2–36). Returns None when the base is
/// out of range or the input contains an invalid digit. Leading whitespace, a
/// sign, and a matching radix prefix (0x/0b/0o) are handled as in the TS.
pub fn parse(input: &str, base: u32) -> Option<BigInt> {
    if !(2..=36).contains(&base) {
        return None;
    }
    let trimmed = input.trim().to_ascii_lowercase();
    let mut s: &str = &trimmed;
    let mut negative = false;
    if let Some(rest) = s.strip_prefix('-') {
        negative = true;
        s = rest;
    } else if let Some(rest) = s.strip_prefix('+') {
        s = rest;
    }
    // A 0x/0b/0o prefix is meaningful only for the matching base.
    if base == 16 {
        s = s.strip_prefix("0x").unwrap_or(s);
    } else if base == 2 {
        s = s.strip_prefix("0b").unwrap_or(s);
    } else if base == 8 {
        s = s.strip_prefix("0o").unwrap_or(s);
    }
    if s.is_empty() {
        return None;
    }

    let mut mag = Mag::zero();
    for &byte in s.as_bytes() {
        let d = digit_value(byte)?;
        if d >= base {
            return None;
        }
        // Horner's method: shift up by one base, then add the new digit.
        mag.mul_small(base);
        mag.add_small(d);
    }
    // Normalize negative zero away so format() never needs to special-case it.
    Some(BigInt {
        mag,
        negative: negative && !mag.is_zero(),
    })
}

/// Format `value` as a lowercase string in `base` (2–36). An out-of-range base
/// yields the empty string; otherwise the sign of zero is never emitted.
pub fn format(value: &BigInt, base: u32) -> String {
    if !(2..=36).contains(&base) {
        return String::new();
    }
    if value.mag.is_zero() {
        return "0".to_string();
    }
    let mut v = value.mag.clone();
    let mut bytes: Vec<u8> = Vec::new();
    while !v.is_zero() {
        let rem = v.divmod_small(base);
        bytes.push(DIGITS[rem as usize]);
    }
    // Digits came out least-significant first; reverse into display order.
    bytes.reverse();
    if value.negative {
        bytes.insert(0, b'-');
    }
    String::from_utf8(bytes).expect("digits are ASCII")
}

/// Parse `value` in `from_base` and re-emit it in `to_base`. Returns None when
/// the input cannot be parsed in `from_base`.
pub fn convert(value: &str, from_base: u32, to_base: u32) -> Option<String> {
    let n = parse(value, from_base)?;
    Some(format(&n, to_base))
}

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 →