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.
Four kinds, one tower
| Kind | Type | What 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. |
Number holds any of the four and does the mixing, so a
literal, a ratio and a float can meet in one expression:
let third = @moonnum.parse("1/3")
@moonnum.parse("1") + third
@moonnum.repr_float(0.1 + 0.2)
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.
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)
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.
@moonnum.parse_int("0xff_ec", base=0)
@moonnum.parse_float("1_000.5e-2")
@moonnum.parse_complex("(3+4j)")
@moonnum.parse_fraction("-1.25")
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.
@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"))
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.
| Spec | Value | Result |
|---|---|---|
015,d | 1234567 | 000,001,234,567 |
#010_x | 255 | 0x000_00ff |
*^12.2f | 3.14159 | ****3.14**** |
.3% | 1/3 | 33.333% |
+.1e | 1.5 | +1.5e+00 |
z | -0.0 | 0.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.
pub(all) suberror Refused {
Malformed(at~ : Int, reason~ : String)
ZeroDivision
Overflow(reason~ : String)
Domain(reason~ : String)
}
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.