Skip to content

Number Base Converter — Swift source

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

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

// number-base — Swift port: arbitrary-precision base conversion (bases 2-36).
import Foundation
/// Arbitrary-precision integer: little-endian base-2^32 limbs; empty == zero.
struct Big {
    var neg = false
    var mag: [UInt32] = []
    var isZero: Bool { mag.isEmpty }

    /// this = this * mul + add — one Horner step over the limbs.
    mutating func mulAdd(_ mul: UInt32, _ add: UInt32) {
        var carry = UInt64(add)
        for i in mag.indices {
            let t = UInt64(mag[i]) * UInt64(mul) + carry
            mag[i] = UInt32(truncatingIfNeeded: t)
            carry = t >> 32
        }
        while carry > 0 { mag.append(UInt32(truncatingIfNeeded: carry)); carry >>= 32 }
    }

    /// this /= d; returns this % d (used to peel least-significant digits).
    mutating func divmod(_ d: UInt32) -> UInt32 {
        var rem: UInt64 = 0
        for i in mag.indices.reversed() {
            let cur = rem << 32 | UInt64(mag[i])
            mag[i] = UInt32(cur / UInt64(d))
            rem = cur % UInt64(d)
        }
        while let last = mag.last, last == 0 { mag.removeLast() }
        return UInt32(rem)
    }
}

private let digits = Array("0123456789abcdefghijklmnopqrstuvwxyz")

/// Map a lowercase character to its value; nil when not a digit.
func digitValue(_ c: Character) -> Int? {
    guard let a = c.asciiValue else { return nil }
    switch a {
    case 48...57: return Int(a - 48) // '0'-'9'
    case 97...122: return Int(a - 87) // 'a'-'z'
    default: return nil
    }
}

/// Parse `s` in `base` (2-36); nil on invalid input. A radix prefix is
/// stripped only when it agrees with the base.
func parseBigInt(_ s: String, base: Int) -> Big? {
    guard (2...36).contains(base) else { return nil }
    var t = s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
    var b = Big()
    if t.hasPrefix("-") { b.neg = true; t.removeFirst() }
    else if t.hasPrefix("+") { t.removeFirst() }
    if (t.hasPrefix("0x") && base == 16) || (t.hasPrefix("0b") && base == 2) ||
        (t.hasPrefix("0o") && base == 8) { t.removeFirst(2) }
    guard !t.isEmpty else { return nil }
    for c in t {
        guard let d = digitValue(c), d < base else { return nil }
        b.mulAdd(UInt32(base), UInt32(d)) // Horner's method
    }
    if b.isZero { b.neg = false } // normalize -0
    return b
}

/// Render `value` in `base` (2-36); "" for an unsupported base. Sign of zero never emitted.
func formatBigInt(_ value: Big, base: Int) -> String {
    guard (2...36).contains(base) else { return "" }
    var t = value
    if t.isZero { return "0" }
    let neg = t.neg
    var out: [Character] = []
    while !t.isZero { out.append(digits[Int(t.divmod(UInt32(base)))]) }
    if neg { out.append("-") }
    return String(out.reversed())
}

/// value(fromBase) -> toBase; nil when it does not parse.
func convertBase(_ value: String, fromBase: Int, toBase: Int) -> String? {
    guard let n = parseBigInt(value, base: fromBase) else { return nil }
    return formatBigInt(n, base: toBase)
}

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 →