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

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 read the tutorial — five pages, one per kind plus the format language, each one runnable as written. The API reference lists every public item, and the design notes explain where the digits come from.