Number Base Converter — Go source
Convert numbers between binary, octal, decimal and hexadecimal. BigInt-powered, so it handles arbitrarily large values without precision loss.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package numberbase is the Go twin of CosmoDev's src/lib/numberBase.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). It performs arbitrary-precision base conversion across bases
// 2–36, exactly like the TS lib. Pure + deterministic, never panics: an
// invalid input yields a false "ok" (mirroring the TS lib's `bigint | null`),
// never a panic. The table-driven tests in number-base_test.go share vectors
// with src/lib/numberBase.test.ts so the two implementations are held to the
// same contract.
//
// The algorithm mirrors the TS lib exactly: trim + lowercase the input → strip
// an optional sign → strip a base-matching 0x/0b/0o prefix → fold each digit
// into an accumulator (value = value*base + digit) using math/big for the same
// arbitrary precision TS gets from BigInt.
package numberbase
import (
"math/big"
"strings"
)
// digits is the shared digit alphabet for bases 2–36. Mirrors DIGITS in
// src/lib/numberBase.ts. Indexed by a digit's numeric value (0–35).
const digits = "0123456789abcdefghijklmnopqrstuvwxyz"
// digitValue maps a single lowercase character to its numeric value (0–35), or
// -1 if it is not a valid digit. Mirrors digitValue() in the TS lib. Inputs are
// already lowercased before this is called (see ParseBigInt), so only '0'–'9'
// and 'a'–'z' are recognized.
func digitValue(ch rune) int {
switch {
case ch >= '0' && ch <= '9':
return int(ch - '0')
case ch >= 'a' && ch <= 'z':
return int(ch-'a') + 10
default:
return -1
}
}
// ParseBigInt parses str in the given base (2–36) into an arbitrary-precision
// integer. It is the Go twin of parseBigInt() in src/lib/numberBase.ts and must
// agree with it on every shared vector.
//
// It trims surrounding whitespace and lowercases the input, accepts an optional
// leading '-' or '+', and strips a '0x'/'0b'/'0o' prefix only when it matches
// the requested base. The returned ok flag is false when base is out of range,
// the input is empty after trimming/prefix removal, or any character is not a
// valid digit for the base — mirroring the TS lib's `null` return.
func ParseBigInt(str string, base int) (*big.Int, bool) {
if base < 2 || base > 36 {
return nil, false
}
s := strings.ToLower(strings.TrimSpace(str))
neg := false
switch {
case strings.HasPrefix(s, "-"):
neg = true
s = s[1:]
case strings.HasPrefix(s, "+"):
s = s[1:]
}
// Strip a base-matching prefix, exactly like the TS lib: '0x' only for
// base 16, '0b' only for base 2, '0o' only for base 8.
if (base == 16 && strings.HasPrefix(s, "0x")) ||
(base == 2 && strings.HasPrefix(s, "0b")) ||
(base == 8 && strings.HasPrefix(s, "0o")) {
s = s[2:]
}
if s == "" {
return nil, false
}
result := new(big.Int)
b := big.NewInt(int64(base))
for _, ch := range s {
d := digitValue(ch)
if d < 0 || d >= base {
return nil, false
}
result.Mul(result, b)
result.Add(result, big.NewInt(int64(d)))
}
if neg {
result.Neg(result)
}
return result, true
}
// FormatBigInt formats value in the given base (2–36). It is the Go twin of
// formatBigInt() in src/lib/numberBase.ts and must agree with it on every
// shared vector.
//
// It returns "" when base is out of range, "0" for a zero value, and prefixes a
// '-' for negatives — matching the TS lib's behavior. Digits above 9 use the
// lowercase alphabet in [digits].
func FormatBigInt(value *big.Int, base int) string {
if base < 2 || base > 36 {
return ""
}
if value.Sign() == 0 {
return "0"
}
v := new(big.Int).Abs(value)
b := big.NewInt(int64(base))
q := new(big.Int)
r := new(big.Int)
// Collect digits least-significant first, then reverse — mirrors the TS
// lib's `out = DIGITS[v % b] + out` prepend without its O(n²) cost.
d := make([]byte, 0, 32)
for v.Sign() > 0 {
q.QuoRem(v, b, r) // v = q*b + r, with 0 <= r < b
d = append(d, digits[r.Int64()])
v.Set(q)
}
for i, j := 0, len(d)-1; i < j; i, j = i+1, j-1 {
d[i], d[j] = d[j], d[i]
}
if value.Sign() < 0 {
return "-" + string(d)
}
return string(d)
}
// ConvertBase converts a value string from one base (2–36) to another. It is
// the Go twin of convertBase() in src/lib/numberBase.ts and must agree with it
// on every shared vector. The returned ok flag is false when the input cannot
// be parsed in fromBase — mirroring the TS lib's `null` return.
func ConvertBase(value string, fromBase, toBase int) (string, bool) {
n, ok := ParseBigInt(value, fromBase)
if !ok {
return "", false
}
return FormatBigInt(n, toBase), true
}
// BaseRow is one (base, text) pair of the live all-bases view — the Go twin
// of the BaseRow interface in src/lib/numberBase.ts.
type BaseRow struct {
Base int
Text string
}
// allBasesDefault mirrors ALL_BASES_DEFAULT in the TS lib: the bases the
// all-bases panel shows.
var allBasesDefault = []int{2, 8, 10, 16, 32, 36}
// AllBases renders the parsed value in several bases at once. It is the Go
// twin of allBases() in src/lib/numberBase.ts and shares its test vectors.
// Returns ok=false when the input does not parse in fromBase (TS: null).
func AllBases(value string, fromBase int, bases []int) ([]BaseRow, bool) {
n, ok := ParseBigInt(value, fromBase)
if !ok {
return nil, false
}
if bases == nil {
bases = allBasesDefault
}
rows := make([]BaseRow, len(bases))
for i, b := range bases {
rows[i] = BaseRow{Base: b, Text: FormatBigInt(n, b)}
}
return rows, true
}
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 →