moonnum.
GuideStart here

One pass through the library

A short pass, in order: install it, read and write the four kinds, format them, then take a real out to whatever width the work needs. Every block below was run, and every line under it is what the run printed.

Step 1. Install, and import

One package, no dependencies of its own. It runs on wasm, wasm-gc, js and native.

moon add moonbitstack/moonnum
import { "moonbitstack/moonnum" }

Step 2. A number, read from text

Reading is where a numeric library is judged: the prefix, the underscore between two digits, the sign, the surrounding space. base=0 means the text says which base it is in.

moonbit
let count = @moonnum.parse_int("0xff_ec", base=0)
println("\{count} and \{count.bit_length()} bits wide")
output
65516 and 16 bits wide

Step 3. Four kinds, one parser

parse reads a literal and gives back a Number of whichever kind the literal spells — an integer, a ratio, a float, a complex number.

moonbit
for text in ["7", "1/3", "0.5", "3+4j"] {
  let value = @moonnum.parse(text)
  println("\{text} -> \{value}")
}
output
7 -> 7
1/3 -> 1/3
0.5 -> 0.5
3+4j -> (3+4j)

Step 4. Mixing them, and comparing exactly

Addition follows the tower — integer, fraction, float, complex — and stops at the first kind that can hold the answer. Comparison does not: an integer and a float are compared as the values they are, not as two floats.

moonbit
let mixed = @moonnum.parse("1") + @moonnum.parse("1/3") + @moonnum.parse("0.5")
println("1 + 1/3 + 0.5 = \{mixed}")
let bigger = @moonnum.parse("9007199254740993").compare_numeric(
  @moonnum.parse("9007199254740992.0"),
) > 0
println("9007199254740993 > 9007199254740992.0 is \{bigger}")
output
1 + 1/3 + 0.5 = 1.8333333333333333
9007199254740993 > 9007199254740992.0 is true

Why the second line matters. As doubles both numbers are 9007199254740992.0, so a float-first comparison answers false. The integer has the larger value and the answer says so.

Step 5. Division that agrees with itself

// rounds towards negative infinity and % takes the sign of the divisor, so the two always reconstruct the dividend. divmod gives both in one call.

moonbit
let (quotient, remainder) = @moonnum.divmod(-7N, 2N)
println("-7 // 2 = \{quotient}, -7 % 2 = \{remainder}")
println("-7 // 2 * 2 + -7 % 2 = \{quotient * 2N + remainder}")
output
-7 // 2 = -4, -7 % 2 = 1
-7 // 2 * 2 + -7 % 2 = -7

Step 6. Powers, roots and counts

With a modulus, a negative exponent is the modular inverse; without one it is refused, because an integer cannot hold the answer. The counting functions are the ones a numeric programmer reaches for next.

moonbit
println(@moonnum.power(3N, -1N, modulus=7N).to_string())
println(@moonnum.power(2N, 1000N, modulus=97N).to_string())
println(@moonnum.isqrt(@moonnum.power(10N, 40N)).to_string())
println(@moonnum.comb(@moonnum.parse_int("100"), 50N).to_string())
println(@moonnum.bit_count(255N).to_string())
output
5
36
100000000000000000000
100891344545564193334812497256
8

Step 7. The same number, written in another base

to_radix is the writing side of parse_int's base, for any base from two to thirty-six. ilog is the exponent of the largest power of the base that still fits.

moonbit
println(@moonnum.to_radix(0x1a2b3cN, radix=32))
println(@moonnum.to_radix(-255N, radix=16))
println(@moonnum.ilog(@moonnum.parse_int("10000000000000000000000000000000000000007")).to_string())
output
1kaps
-ff
40

Step 8. A float, read and written honestly

repr_float is the shortest text that reads back as the same double. The exact value is one call away, and it is not the decimal it looks like.

moonbit
println(@moonnum.repr_float(0.1))
println(@moonnum.repr_float(0.1 + 0.2))
println(@moonnum.float_hex(0.1))
let (numerator, denominator) = @moonnum.as_integer_ratio(0.1)
println("\{numerator}/\{denominator}")
println(@moonnum.is_integer(2.5).to_string())
output
0.1
0.30000000000000004
0x1.999999999999ap-4
3602879701896397/36028797018963968
false

Step 9. Rounding is a separate decision

round_float rounds halves to even, on the value rather than on the text. Printing a number and rounding it are two different calls, and neither happens behind your back.

moonbit
println(@moonnum.round_float(2.675, ndigits=2).to_string())
println(@moonnum.round_float(0.5, ndigits=0).to_string())
println(@moonnum.round_float(1.5, ndigits=0).to_string())
output
2.67
0
2

Step 10. Complex numbers, written the way people write them

3+4j reads, and the same spelling comes back out — including the negative zero, which is part of the value and not a formatting accident.

moonbit
let z = @moonnum.parse_complex("3+4j")
println(z.to_string())
println((z * z).to_string())
println(z.abs().to_string())
println(@moonnum.repr_complex(@moonnum.parse_complex("-0-1j")))
output
(3+4j)
(-7+24j)
5
(-0-1j)

Step 11. Fractions that stay exact

A fraction is a numerator over a denominator in lowest terms with a positive denominator. Arithmetic on them is exact; limit_denominator is the one place it deliberately is not.

moonbit
let third = @moonnum.parse_fraction("1/3")
println((third + third + third).to_string())
println(@moonnum.parse_fraction("0.1").to_string())
println(third.limit_denominator(max=100N).to_string())
println(@moonnum.format(@moonnum.parse("22/7"), ".6f"))
output
1
1/10
1/3
3.142857

Step 12. One format language for every kind

Fill, alignment, sign, the alternate form, zero padding that joins the grouping, and a type letter for each kind. The same specification works on integers, floats, complex numbers and fractions.

moonbit
println(@moonnum.format_int(1234567N, "015,d"))
println(@moonnum.format_int(255N, "#010_x"))
println(@moonnum.format_float(0.25, ".2%"))
println(@moonnum.format_float(-0.0, "z"))
println(@moonnum.format_float(1234.5678, ">12.2f"))
output
000,001,234,567
0x000_00ff
25.00%
0.0
     1234.57

Step 13. A real at a width you choose

Fixed is mantissa / 2^bits: every step truncates to one grid, so a whole computation carries one width. Fifty digits of it are enough to see the point.

moonbit
let bits = @moonnum.Fixed::bits_for_digits(50)
let two = @moonnum.Fixed::of_int(2, bits)
println(two.sqrt().to_string())
println((two * @moonnum.Fixed::pi(bits)).to_decimal(10))
output
1.41421356237309504880168872420969807856967187537695
6.2831853072

Step 14. A real at any width

BigFloat keeps the width on the value, so a number carries the precision it was made at wherever it sits on the line. Two hundred bits, and the wider field that comes with it: a cube root, the common logarithm, and a whole power taken by squaring.

moonbit
let c = @moonnum.Context::new(200)
let big = @moonnum.BigFloat::of_int(2, context=c)
println(big.sqrt(context=c).to_significant(40))
println(@moonnum.BigFloat::pi(context=c).to_significant(40))
println(big.cbrt(context=c).to_significant(40))
println(@moonnum.BigFloat::ln10(context=c).to_significant(40))
println(big.powi(10N, context=c).to_significant(40))
output
1.41421356237309504880168872420969807857
3.141592653589793238462643383279502884197
1.25992104989487316476721060727822835057
2.302585092994045684017991454684364207601
1024.0

Step 15. When the answer is complex

A function whose result is real answers nan for a value outside the reals, rather than quietly returning something of another type. The value exists anyway, one promotion away, written out at the call site.

moonbit
let minus_one = -@moonnum.BigFloat::one(context=c)
println(minus_one.sqrt(context=c).to_string())
println(minus_one.to_complex().sqrt(context=c).to_string())
output
nan
(0.0+1.0j)

Step 16. A refusal says no, and says why

Every fallible call raises one error type. A parse failure names the offset where the text stopped making sense, counted in UTF-16 code units so it can be used to slice the input.

moonbit
let refused = @moonnum.parse_int("12x", base=0).to_string() catch {
  @moonnum.Malformed(at~, reason~) => "\{reason} at \{at}"
  _ => "refused"
}
println(refused)
output
unexpected trailing character at 2

Where to go next

Each of the four kinds has a chapter of its own, with the cases the tour skipped: integers, floats, complex numbers, fractions, the format language and the real of any width. The design notes say where the digits come from, and the API reference lists every public item.