Skip to content

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 →