Skip to content

Numeral Systems

A number and its digits are different things. IntegerDigits(10, 2) is [1,0,1,0], but the ten did not change — only the spelling. A numeral system is a bijection between integers and digit strings, and once you say it that way it is clear that base-b is one member of a much larger family.

compute-engine already has IntegerDigits and FromDigits, and they already do fixed radix. @enumeratio/numerals adds no head for that — it widens the base slot to take a system, the way Wolfram's IntegerDigits[n, MixedRadix[…]] does. An integer base still goes to the native handler, unchanged.

One slot, many systems

SystemDigitsNotes
2, 16, …0…b−1compute-engine's own, untouched
MixedRadix([…])a different bound per placedays/hours/minutes/seconds
Factoradicplace k holds at most kthe Lehmer code
PrimorialRadixplace k below the (k+1)-th primefactoradic with primes
BalancedRadix(b)−(b−1)/2 … (b−1)/2signless negatives
NegativeRadix(b)0…b−1 over (−b)^ksignless negatives
BijectiveRadix(k)1…k, no zero digitspreadsheet columns
Zeckendorfbinary over Fibonacci placesno two adjacent ones
CombinatorialSystem(k)a strictly decreasing k-tuplek-subset unranking
ResidueSystem([…])independent residuesno place values at all
Ostrowski([…])digits over a continued fractiongeneralises Zeckendorf
AdicNumerals(b, prec?)base b, infinite to the LEFTnegatives are all nines

Four of those are not "base-b with a twist": Zeckendorf and Ostrowski constrain digits by a forbidden pattern instead of a per-place bound, the residue system has no place values, and two of them represent negative integers with no sign. The last one is base b read in the other direction — digits that stop on the right and never on the left — and it comes with a value type and arithmetic of its own: b-adic numbers.

The base slot is a system

93 784 seconds is 1 day, 2 hours, 3 minutes, 4 seconds.

source
<notatio-cell value="IntegerDigits(93784, MixedRadix([24, 60, 60]))" />
<notatio-cell value="FromDigits([1, 2, 3, 4], MixedRadix([24, 60, 60]))" />
<notatio-cell value="IntegerDigits(255, 16)" />

Two of these systems are unranking maps in disguise

This is the part that earns numeral systems a place next to the collections.

Factoradic digits are the Lehmer code. Writing n in the system whose place values are the factorials, and unranking the n-th permutation in lexicographic order, are the same computation. Pad to one digit per position and the two line up exactly:

Factoradic = Lehmer code

The padded factoradic digits of 5, and the 6th permutation of four things. The code says: take item 0, then item 2 of what is left, then item 1, then the last.

source
<notatio-cell value="IntegerDigits(5, Factoradic, 4)" />
<notatio-cell value="SymmetricGroup(4)[6]" />

The combinatorial number system is k-subset unranking. Its digits are the subset: n = C(c_k,k) + … + C(c_1,1), and the c's — shifted from 0-based to 1-based — are exactly the n-th k-subset in colexicographic order.

Combinatorial system = KSubsets

Digits [3,2,1] are the 0-based subset; add one to each and you get {2,3,4}, which is the 4th 3-subset.

source
<notatio-cell value="IntegerDigits(3, CombinatorialSystem(3))" />
<notatio-cell value="KSubsets(5, 3)[4]" />

So "write this number in that system" and "give me the n-th object of that kind" are one question asked twice. A mixed-radix numeral is a lexicographic rank.

Negatives without a sign

Balanced ternary and negabinary both represent every integer — negative ones included — with no sign and no two's-complement convention. In balanced ternary, negating a number is negating each digit.

Signless

5 is 9−3−1, and −5 is its digit-wise negation. Compare the last cell: fixed radix has to drop the sign.

source
<notatio-cell value="IntegerDigits(5, BalancedRadix(3))" />
<notatio-cell value="IntegerDigits(-5, BalancedRadix(3))" />
<notatio-cell value="IntegerDigits(3, NegativeRadix(2))" />
<notatio-cell value="IntegerDigits(-5, 2)" />

A forbidden pattern instead of a bound

Zeckendorf's theorem: every positive integer is a sum of non-consecutive Fibonacci numbers, in exactly one way — and greedy finds it. So the digits are binary, but what makes a string a numeral is that no two ones are adjacent, not a per-place range. A string with two adjacent ones denotes nothing, and FromDigits says so by declining.

Zeckendorf

100 = 89 + 8 + 3. The last cell is not a numeral, so it is left standing.

source
<notatio-cell value="IntegerDigits(100, Zeckendorf)" />
<notatio-cell value="FromDigits([1, 0, 0, 0, 0, 1, 0, 1, 0, 0], Zeckendorf)" />
<notatio-cell value="FromDigits([1, 1], Zeckendorf)" />

Zeckendorf is one continued fraction's worth

Ostrowski numeration is the general form. Take any irrational α with continued fraction ; the denominators of its convergents satisfy , and those are the place values. Every integer in range then has exactly one representation

subject to , , and — the rule that does the real work — whenever reaches its ceiling .

That last condition is what forbids a carry, and it is the general shape of "no two adjacent ones". Feed it the all-ones continued fraction, which is , and the are the Fibonacci numbers and the ceiling rule collapses to Zeckendorf's. So Zeckendorf is the case — and the package checks the two against each other rather than taking that on trust.

A continued fraction as a numeral system

All quotients 1 is φ, and the digits are Zeckendorf's with one extra forced zero.

source
<notatio-cell value="IntegerDigits(20, Ostrowski([1, 1, 1, 1, 1, 1, 1, 1]))" />
<notatio-cell value="IntegerDigits(20, Zeckendorf)" />
<notatio-cell value="IntegerDigits(9, Ostrowski([2, 2, 2]))" />
<notatio-cell value="FromDigits([1, 2, 1], Ostrowski([2, 2, 2]))" />

The last cell declines: the middle digit is already at its ceiling, so nothing below it may be non-zero. Which continued fraction you pick changes the arithmetic completely — this is the same that names elements of the modular group, wearing a different hat.

No place values at all

A residue number system writes n as its remainders against a list of moduli. There are no place values and no order — the digits are independent, which is exactly why addition and multiplication are carry-free and can be done in parallel, one channel at a time. By CRT it is a bijection onto [0, ∏mᵢ) precisely when the moduli are pairwise coprime; otherwise it is not a numeral system at all, and a digit string can be inconsistent.

Carry-free arithmetic

23 + 41 = 64, done channel by channel: (2+2, 3+1, 2+6) reduced is (1,4,1), which reads back as 64 — no carries between channels.

source
<notatio-cell value="IntegerDigits(23, ResidueSystem([3, 5, 7]))" />
<notatio-cell value="IntegerDigits(41, ResidueSystem([3, 5, 7]))" />
<notatio-cell value="FromDigits([1, 4, 1], ResidueSystem([3, 5, 7]))" />
<notatio-cell value="NumeralSystemShape(ResidueSystem([4, 6]))" />

These are the same CRT channels the hypercomplex page uses to find split units: a residue numeral and a spectral sign vector are the same object, read for different purposes.

Bijective base 26 is spreadsheet columns

Digits 1…26 with no zero DIGIT — but zero itself is the empty numeral, which is what makes the correspondence a bijection: one string over per non-negative integer, and the empty one is left for zero. So every positive integer has exactly one spelling and there are no leading-zero ambiguities. Map 1…26 to A…Z and you get the column lettering: 26 is Z, 27 is AA, 702 is ZZ, 703 is AAA. Note that there is no numeral for zero at all.

A, …, Z, AA, …

source
<notatio-cell value="IntegerDigits(26, BijectiveRadix(26))" />
<notatio-cell value="IntegerDigits(27, BijectiveRadix(26))" />
<notatio-cell value="IntegerDigits(703, BijectiveRadix(26))" />
<notatio-cell value="IntegerDigits(0, BijectiveRadix(26))" />

Things worth knowing

The round trip is the specification. A numeral system is a bijection, so the only property worth testing is that FromDigits(IntegerDigits(n, S), S) is n — and that is what the tests do, for every system, over a range, rather than pinning spot values. Each system's digit shape is then characterised separately and independently: factoradic digits satisfy d_k ≤ k, Zeckendorf digits have no adjacent ones, balanced digits are bounded by ±(b−1)/2, bijective digits avoid zero.

Declining is an answer. An integer with no numeral in a system (zero in bijective base, anything past ∏mᵢ in a residue system) and a digit string that denotes no integer (adjacent Zeckendorf ones, an out-of-range mixed-radix digit, inconsistent residues) both leave the call standing rather than returning something false.

Nothing native changed. IntegerDigits(n, b) for an integer b is still compute-engine's own handler, and a test pins that against a bare engine.

Not built yet. Non-integer bases — golden-ratio base φ is the interesting one, where the digit string of an integer is infinite to the right unless you allow a fractional part. Also no digit rendering: BaseForm exists natively and could learn these systems, and bijective base 26 obviously wants to print as letters rather than as a list of numbers.