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 →