moonnum.

A number library for MoonBit

Numbers that say what they are.

Arbitrary-precision integers, IEEE-754 floats printed honestly, complex numbers written 3+4j, and exact fractions. One package, one error type, one formatting language.

as a float 0.1
as a fraction 1/10
what the double holds 3602879701896397/36028797018963968
and 0.1 + 0.2 0.30000000000000004

Four kinds, one tower

KindTypeWhat it is
Integer @bigint.BigInt Core's arbitrary-precision integer, with what it is missing: floor division, a modulo that takes the divisor's sign, modular powers, isqrt, comb, perm, bytes.
Float Double Binary64, read and written so that the text is the shortest one that reads back — and the exact value one query away.
Complex Complex { re, im } of doubles, with the signed-zero, infinity and NaN rules written down rather than hoped for.
Fraction Fraction { num, den }, always in lowest terms with a positive denominator, so arithmetic never drifts.
Int BigInt · unbounded Fraction num/den · exact Float binary64 Complex two doubles 1 == 1.0 == 3/3 == 1+0j — comparison is exact at every step
Promotion only ever goes right. Comparison never rounds.

Number holds any of the four and does the mixing, so a literal, a ratio and a float can meet in one expression:

moonbit
let third = @moonnum.parse("1/3")
@moonnum.parse("1") + third
@moonnum.repr_float(0.1 + 0.2)
output
4/3
0.30000000000000004

A fifth type, for the case in between

The four kinds are widthless: an integer has as many digits as it needs, a float has exactly fifty-three, a fraction is exact or is not. There is one more type for the case in between — a real carried to a width the caller picks, with the width kept on the value and a direction for the bits that do not fit.

moonbit
let c = @moonnum.Context::new(200)
@moonnum.BigFloat::of_double(0.1)
@moonnum.BigFloat::of_double(0.1).to_decimal(40)
@moonnum.BigFloat::parse("0.1", context=c).to_decimal(40)
@moonnum.BigFloat::pi(context=c).to_significant(40)
output
0.1
0.1000000000000000055511151231257827021182
0.1000000000000000000000000000000000000000
3.141592653589793238462643383279502884197

The first line is the double nearest a tenth, written as the shortest text that reads back as it; the second is that same double written out, where the digits repr_float keeps to itself are on show. The third is a tenth read at two hundred bits, which is a tenth to forty places instead of a tenth to sixteen. The fourth is a constant nothing typed in: the library computes pi from a formula, and there is no table of digits in it to go stale. One page on it — and where a real function has to answer nan, like the square root of a negative, the value it could not hold is one promotion away, in BigComplex, at the same width.

Reading and writing

Every parser takes the spellings you already type: base prefixes, underscores between digits, surrounding whitespace, inf and nan in any case, 1+2j and 3+5i. Refusals carry the offset where the text stopped making sense.

moonbit
@moonnum.parse_int("0xff_ec", base=0)
@moonnum.parse_float("1_000.5e-2")
@moonnum.parse_complex("(3+4j)")
@moonnum.parse_fraction("-1.25")
output
65516
10.005
(3+4j)
-5/4

Writing is the other half, and the half that is usually wrong. repr_float gives the shortest string that reads back as the same double, and switches to an exponent exactly where the digits would start lying.

moonbit
@moonnum.repr_float(1.0e15)
@moonnum.repr_float(1.0e16)
@moonnum.repr_float(1.0e-5)
@moonnum.float_hex(0.1)
@moonnum.repr_complex(@moonnum.parse_complex("-0-1j"))
output
1000000000000000.0
1e+16
1e-05
0x1.999999999999ap-4
(-0-1j)

One formatting language

Fill, alignment, sign, z, #, zero padding, width, grouping and precision, with the presentation types b c d e E f F g G n o x X %. The same specifier works on every kind, including the exact ones.

SpecValueResult
015,d1234567000,001,234,567
#010_x2550x000_00ff
*^12.2f3.14159****3.14****
.3%1/333.333%
+.1e1.5+1.5e+00
z-0.00.0

Zero padding participates in grouping, z acts before the sign is chosen, and # keeps the zeros g would drop. Those three sentences are the whole difference between a formatter and a correct formatter, and each is a test.

Failure is a value

One error type, four cases, no panics. Division is a function rather than an operator, because a numeric library that aborts on zero is worse than one that says so.

moonbit
pub(all) suberror Refused {
  Malformed(at~ : Int, reason~ : String)
  ZeroDivision
  Overflow(reason~ : String)
  Domain(reason~ : String)
}
what it means
text stopped making sense
   at this UTF-16 offset
1/0, 1 % 0, Fraction(1, 0)
does not fit the target type
defined input, no answer

Start

moon add moonbitstack/moonnum

Then take the guided tour — one pass from the first number to a real of any width, every block run and every line of output the line it printed. The tutorial has a chapter per kind for the cases the tour skips, the API reference lists every public item, and the design notes explain where the digits come from. Or try the numbers in the browser: the panels run this library, compiled to JavaScript.