Sign in

    moonnum

    moonnum — Python's numbers for MoonBit: arbitrary-precision integers, floats, complex numbers and fractions, read, written and formatted as Python reads, writes and formats them. It is not a decimal type and not a bignum encoding.

    bigint
    float
    complex
    fraction
    format
    number
    moonbit
    Download zip
    Version
    0.2.1
    License
    Apache-2.0
    Last updated
    3 days ago
    Downloads
    24

    No dependencies

    #moonnum

    Numbers in MoonBit that behave the way you expect them to.

    Arbitrary-precision integers, IEEE-754 floats with honest text, complex numbers written 3+4j, and exact fractions — one package, one error type, one formatting language.

    let n = @moonnum.parse_int("0xff_ec", base=0) // 65516, any base, any size
    let z = @moonnum.parse_complex("3+4j") // (3+4j)
    let f = @moonnum.parse_fraction("0.1") // 1/10, exactly

    @moonnum.repr_float(0.1 + 0.2) // "0.30000000000000004"
    @moonnum.format(f, ".3%") // "10.000%"

    Nothing in here rounds behind your back. 0.1 as a fraction is one tenth; 0.1 as a float is the double it really is, and says so when asked:

    @moonnum.repr_float(0.1) // "0.1" shortest string that reads back
    @moonnum.float_hex(0.1) // "0x1.999999999999ap-4"
    @moonnum.as_ratio(0.1) // 3602879701896397/36028797018963968
    @moonnum.repr_float(0.1 + 0.2) // "0.30000000000000004"
    @moonnum.format(@moonnum.parse("22/7"), ".6f") // "3.142857", exactly rounded

    #Install

    moon add moonbitstack/moonnum

    import { "moonbitstack/moonnum" }

    Runs on wasm, wasm-gc, js and native.

    #The four kinds

    Number holds any of the four and does the mixing:

    let third = @moonnum.parse("1/3")
    let total = @moonnum.parse("1") + third + @moonnum.parse("0.5") // 11/6
    let judged = @moonnum.parse("1") == @moonnum.parse("1.0") // true

    Promotion follows the tower — integer, fraction, float, complex — and comparison is exact: 9007199254740993 is greater than 9007199254740992.0, which a float-first comparison gets wrong.

    #Arithmetic

    OnFunctions
    integersfloordiv, floor_mod, divmod, power (with modulus~), gcd, lcm, isqrt, factorial, comb, perm, bit_length, to_bytes, of_bytes
    floatsround_float(d, ndigits~), round_big, floor_big, ceil_big, trunc_big, as_ratio, as_integer_ratio
    complex+ - *, Complex::divide, Complex::pow, abs, conjugate
    fractions+ - *, Fraction::divide, Fraction::pow, compare, floor_big, ceil_big, round, round_big, to_double, limit_denominator
    any of themNumber::add, Number::sub, Number::mul, Number::divide, Number::pow, compare_numeric, equal_numeric

    Fraction::pow takes an integer exponent and stays a ratio — (2/3) ** 100 is an exact numerator over an exact denominator — while Number::pow follows the tower and gives a float when an exponent is one.

    #Reading

    FunctionReads
    parse_int(text, base~ = 10)int(s, base): prefixes when base is 0, single underscores between digits, surrounding whitespace
    parse_float(text)float(s): inf, infinity, nan in any case, decimal and exponent forms, underscores
    parse_complex(text)complex(s): 1+2j, (1+2j), j, -j, 1e3j — and 3+5i
    parse_fraction(text)Fraction(s): 3/4, -1.25, 1e-3, 7
    parse(text)a Number of whichever kind the literal spells

    #Writing

    FunctionWrites
    repr_float(d)the shortest string that reads back, switching to an exponent at 1e16 and 1e-5
    float_hex(d) / float_of_hex(s)the exact hexadecimal form, and back
    repr_complex(z), repr_fraction(f)(1+2j), 2j, Fraction(3, 4)
    format(value, spec)the format-specification mini-language, for any kind

    #The format language

    [[fill]align][sign][z][#][0][width][grouping][.precision][type]

    @moonnum.format_int(1234567N, "015,d") // "000,001,234,567"
    @moonnum.format_int(255N, "#010_x") // "0x000_00ff"
    @moonnum.format_float(0.25, ".2%") // "25.00%"
    @moonnum.format_float(-0.0, "z") // "0.0"
    z.format(".2f") // "3.00+4.00j"

    Types b c d e E f F g G n o x X % and no type at all; fill and alignment < > ^ =; sign + - space; # alternate form; 0 zero padding that participates in grouping; grouping , and _, four digits to a group for binary, octal and hexadecimal.

    #Arithmetic that can fail, fails on purpose

    One error type, four cases, no panics:

    pub(all) suberror Refused {
    Malformed(at~ : Int, reason~ : String) // where the text stopped making sense
    ZeroDivision
    Overflow(reason~ : String)
    Domain(reason~ : String)
    }

    let half = @moonnum.parse_fraction("1/2")
    let bad : String = half.divide(@moonnum.parse_fraction("0")).to_string() catch {
    @moonnum.ZeroDivision => "no"
    _ => "no"
    }

    at counts UTF-16 code units, so it can be used to slice the input directly. Division is a function and not an operator (Fraction::divide, Complex::divide) because MoonBit's Div trait cannot raise, and a numeric library that panics on zero is worse than one that says so.

    #What this is not

    • Not a decimal type. Decimal has a context, signals and rounding modes; that is a different problem.
    • Not a bignum encoding. Variable-length integer encodings live elsewhere.
    • Not complex math. cmath's functions are out until something needs them.

    #Documentation

    • Tutorial — five pages, one per kind plus formatting. Every output block on them was produced by running the code above it, including the refusals.
    • API reference — every public item.
    • Design notes — how the digits are produced, and why the tower compares exactly.
    • src/examples/ — moon run src/examples/01-integers and friends.

    #Licence

    Apache-2.0.

    Refused

    pub(all) suberror Refused {
    Malformed(at~ : Int, reason~ : String)
    ZeroDivision
    Overflow(reason~ : String)
    Domain(reason~ : String)
    }

    Everything this library refuses, in one type.

    Malformed carries the offset where the input stopped making sense, in UTF-16 code units — the unit StringView indexes by — so a caller can point at the character instead of at the string.
    impl Show for Refused

    Refused::output

    fn Refused::output(self : Refused, logger : &Logger) -> Unit

    Refused::to_string

    fn Refused::to_string(self : Refused) -> String

    Complex

    pub(all) struct Complex {
    re : Double
    im : Double
    }

    A complex number: z.re + z.im * j.

    Adds, subtracts, multiplies, negates and compares with ==; division is Complex::divide because it can refuse a zero divisor.
    impl Add for Complex
    impl Default for Complex
    impl Eq for Complex
    impl Mul for Complex
    impl Neg for Complex
    impl Show for Complex
    impl Sub for Complex

    Complex::abs

    fn Complex::abs(self : Complex) -> Double

    abs(z): the Euclidean norm, infinity when either part is infinite.

    Complex::add

    fn Complex::add(self : Complex, other : Complex) -> Complex

    Complex::conjugate

    fn Complex::conjugate(self : Complex) -> Complex

    z.conjugate().

    Complex::default

    fn Complex::default() -> Complex

    Complex::divide

    fn Complex::divide(self : Complex, other : Complex) -> Complex raise Refused

    self / other.

    A function rather than an operator, for the same reason as Fraction::divide: division by zero has to be reported, and Div cannot.

    Complex::equal

    fn Complex::equal(self : Complex, other : Complex) -> Bool

    Complex::format

    fn Complex::format(self : Complex, spec_text : StringView) -> String raise Refused

    Format a complex number: the real part with the specifier's sign, the imaginary part forced positive, then j, in parentheses unless the real part is a positive zero.

    Complex::j

    fn Complex::j() -> Complex

    The imaginary unit.

    Complex::mul

    fn Complex::mul(self : Complex, other : Complex) -> Complex

    Complex::neg

    fn Complex::neg(self : Complex) -> Complex

    Complex::new

    fn Complex::new(re : Double, im : Double) -> Complex

    Complex(re, im).

    Complex::not_equal

    fn Complex::not_equal(x : Complex, y : Complex) -> Bool

    Complex::output

    fn Complex::output(self : Complex, logger : &Logger) -> Unit

    Complex::pow

    fn Complex::pow(self : Complex, w : Complex) -> Complex raise Refused

    z ** w, including Python's special cases.

    An exponent that is a small integer is applied by repeated squaring, as CPython does; anything else goes through the polar form, which is why (1+1j) ** 2 is exactly 2j and (1+1j) ** 2.0 is not.

    Complex::sub

    fn Complex::sub(self : Complex, other : Complex) -> Complex

    Complex::to_string

    fn Complex::to_string(self : Complex) -> String

    Fraction

    A rational number. den is always positive and gcd(num, den) is always 1, so num and den can be read directly.

    Adds, subtracts, multiplies, negates, compares with == and an ordering; division is Fraction::divide because it can refuse a zero divisor.
    impl Add for Fraction
    impl Compare for Fraction
    impl Default for Fraction
    impl Eq for Fraction
    impl Mul for Fraction
    impl Neg for Fraction
    impl Show for Fraction
    impl Sub for Fraction

    Fraction::abs

    fn Fraction::abs(self : Fraction) -> Fraction

    The same value without its sign.

    Fraction::add

    fn Fraction::add(self : Fraction, other : Fraction) -> Fraction

    Fraction::ceil_big

    math.ceil, as an integer.

    Fraction::compare

    fn Fraction::compare(self : Fraction, other : Fraction) -> Int

    Fraction::default

    fn Fraction::default() -> Fraction

    Fraction::denominator

    The denominator, always positive.

    Fraction::divide

    fn Fraction::divide(self : Fraction, other : Fraction) -> Fraction raise Refused

    self / other.

    Division is a function and not an operator here: MoonBit's Div trait cannot raise, and a fraction that silently divides by zero or panics would be worse than one that says ZeroDivision.

    Fraction::equal

    fn Fraction::equal(self : Fraction, other : Fraction) -> Bool

    Fraction::floor_big

    math.floor, as an integer.

    Fraction::floor_divide_big

    fn Fraction::floor_divide_big(self : Fraction, other : Fraction) ->
    BigInt
    raise Refused

    self // other as an integer: Python's floor division of two fractions.

    Fraction::format

    fn Fraction::format(self : Fraction, spec_text : StringView) -> String raise Refused

    Format a fraction. Without a presentation type it is a ratio, with a digit grouping separator allowed in both halves; with a float type it is the exact rational rounded to the requested digits.

    Fraction::is_integer

    fn Fraction::is_integer(self : Fraction) -> Bool

    Whether the value is a whole number.

    Fraction::limit_denominator

    fn Fraction::limit_denominator(self : Fraction, max~ :
    BigInt
    ) -> Fraction raise Refused

    The closest fraction whose denominator is at most max.

    Fraction::modulo

    fn Fraction::modulo(self : Fraction, other : Fraction) -> Fraction raise Refused

    self % other: the remainder that takes the sign of the divisor.

    Fraction::mul

    fn Fraction::mul(self : Fraction, other : Fraction) -> Fraction

    Fraction::neg

    fn Fraction::neg(self : Fraction) -> Fraction

    Fraction::not_equal

    fn Fraction::not_equal(x : Fraction, y : Fraction) -> Bool

    Fraction::numerator

    The numerator, with the sign.

    Fraction::of_float

    fn Fraction::of_float(d : Double) -> Fraction raise Refused

    The exact value of a float, as a fraction. 0.1 is not 1/10 here; it is 3602879701896397/36028797018963968, which is what the double holds.

    Fraction::of_int

    The integer n as a fraction.

    Fraction::op_ge

    fn Fraction::op_ge(x : Fraction, y : Fraction) -> Bool

    Fraction::op_gt

    fn Fraction::op_gt(x : Fraction, y : Fraction) -> Bool

    Fraction::op_le

    fn Fraction::op_le(x : Fraction, y : Fraction) -> Bool

    Fraction::op_lt

    fn Fraction::op_lt(x : Fraction, y : Fraction) -> Bool

    Fraction::output

    fn Fraction::output(self : Fraction, logger : &Logger) -> Unit

    Fraction::pow

    self ** exp for an integer exponent.

    The exponent is applied to the numerator and the denominator separately, so (2/3) ** 100 is one exact ratio and not a double that nearly is. A negative exponent reciprocates, which is why 0 ** -1 is ZeroDivision and 0 ** 0 is 1, as in Python.

    Fraction::reciprocal

    fn Fraction::reciprocal(self : Fraction) -> Fraction raise Refused

    1/self.

    Fraction::round

    fn Fraction::round(self : Fraction, ndigits~ : Int) -> Fraction

    round(self, ndigits): round to ndigits decimal places, ties to even.

    Fraction::round_big

    round(self): the nearest integer, ties to even.

    Fraction::sub

    fn Fraction::sub(self : Fraction, other : Fraction) -> Fraction

    Fraction::to_double

    fn Fraction::to_double(self : Fraction) -> Double raise Refused

    The nearest double, or Overflow when the value does not fit.

    Fraction::to_string

    fn Fraction::to_string(self : Fraction) -> String

    Number

    pub(all) enum Number {
    Int(
    BigInt
    )
    Float(Double)
    Fraction(Fraction)
    Complex(Complex)
    }

    A number of any kind, as the tower arranges them.

    Promotion goes Int → Fraction → Float → Complex. Comparison is exact across the tower: an integer is never rounded to a float to be compared with one.
    impl Add for Number
    impl Eq for Number
    impl Mul for Number
    impl Neg for Number
    impl Show for Number
    impl Sub for Number

    Number::add

    fn Number::add(self : Number, other : Number) -> Number

    Number::compare_numeric

    fn Number::compare_numeric(self : Number, other : Number) -> Int raise Refused

    Compare two real numbers, exactly.

    Raises Domain for a NaN operand and for a complex number with a non-zero imaginary part, which Python refuses to order.

    Number::divide

    fn Number::divide(self : Number, other : Number) -> Number raise Refused

    a / b, with Python's result kinds: two integers divide to a float, a fraction stays exact, and a complex number is complex.

    Number::equal

    fn Number::equal(self : Number, other : Number) -> Bool

    Number::equal_numeric

    fn Number::equal_numeric(self : Number, other : Number) -> Bool

    a == b, across the tower: 1 == 1.0 == Fraction(1) == 1 + 0j.

    Number::floor_mod

    fn Number::floor_mod(self : Number, other : Number) -> Number raise Refused

    a % b: the remainder that takes the sign of the divisor.

    Number::floordiv

    fn Number::floordiv(self : Number, other : Number) -> Number raise Refused

    a // b.

    Number::mul

    fn Number::mul(self : Number, other : Number) -> Number

    Number::neg

    fn Number::neg(self : Number) -> Number

    Number::not_equal

    fn Number::not_equal(x : Number, y : Number) -> Bool

    Number::output

    fn Number::output(self : Number, logger : &Logger) -> Unit

    Number::pow

    fn Number::pow(self : Number, exponent : Number) -> Number raise Refused

    a ** b.

    Number::sub

    fn Number::sub(self : Number, other : Number) -> Number

    Number::to_double

    fn Number::to_double(self : Number) -> Double raise Refused

    The value as a double, or Overflow when it is infinite or not real.

    Number::to_string

    fn Number::to_string(self : Number) -> String

    as_integer_ratio

    float.as_integer_ratio: the exact numerator and denominator of d.

    as_ratio

    fn as_ratio(d : Double) -> Fraction raise Refused

    d as an exact fraction.

    bit_length

    fn bit_length(n :
    BigInt
    ) -> Int

    The number of bits needed to represent n, sign excluded: int.bit_length.

    ceil_big

    fn ceil_big(d : Double) ->
    BigInt
    raise Refused

    math.ceil(d), as an integer.

    comb

    math.comb(n, k): the number of ways to choose k from n without repetition.

    float_hex

    fn float_hex(d : Double) -> String

    The hexadecimal form of a double: float.hex.

    float_of_hex

    fn float_of_hex(text : StringView) -> Double raise Refused

    float.fromhex: the double a hexadecimal literal denotes.

    The 0x prefix is optional, the exponent is decimal and introduced by p, and the rounding is the same half-to-even as every other conversion here.

    floor_big

    fn floor_big(d : Double) ->
    BigInt
    raise Refused

    math.floor(d), as an integer.

    floor_mod

    a % b: the remainder that takes the sign of the divisor.

    floordiv

    a // b: the quotient rounded towards negative infinity.

    format

    fn format(value : Number, spec_text : StringView) -> String raise Refused

    format(value, spec), for any number.

    format_float

    fn format_float(d : Double, spec_text : StringView) -> String raise Refused

    Format a double the way Python's format() does.

    format_int

    fn format_int(n :
    BigInt
    , spec_text : StringView) -> String raise Refused

    Format an integer, including the float types Python allows on integers by converting them.

    isqrt

    math.isqrt(n): the floor of the square root.

    lcm

    The least common multiple, always non-negative; zero when either side is.

    of_bytes

    fn of_bytes(bytes : BytesView, big? : Bool, signed? : Bool) ->
    BigInt

    int.from_bytes: the integer held by bytes, big-endian unless big~ = false, two's complement when signed~ = true.

    parse

    fn parse(text : StringView) -> Number raise Refused

    parse(text): read a Python number literal and keep the kind it spelled.

    A / makes a fraction, a trailing j or i makes a complex number, a point or an exponent or inf/nan makes a float, and anything else is an integer in whatever base its prefix names.

    parse_complex

    fn parse_complex(text : StringView) -> Complex raise Refused

    complex(text): 1+2j, (1+2j), j, -j, 1e3j, and 3+5i.

    The i suffix is this library's addition; Python only reads j.

    parse_float

    fn parse_float(text : StringView) -> Double raise Refused

    float(text): Python's float literal syntax.

    Accepts inf, infinity and nan in any case with an optional sign, decimal forms with or without a fractional part or an exponent, single underscores between digits, and surrounding whitespace. Hexadecimal literals are not float syntax; float_of_hex reads those.

    parse_fraction

    fn parse_fraction(text : StringView) -> Fraction raise Refused

    Fraction(text): "3/4", "-1.25", "1e-3", "7".

    Whitespace is allowed around the sign, the slash and the ends; digits may be separated by single underscores. The decimal and exponent forms are read as exact powers of ten, never through a float.

    parse_int

    fn parse_int(text : StringView, base? : Int) ->
    BigInt
    raise Refused

    int(text, base): an integer in any base from 2 to 36, with Python's spelling rules.

    Surrounding whitespace is allowed, the sign is optional, digits may be separated by single underscores, and base = 0 means "read the prefix": 0x for hexadecimal, 0o for octal, 0b for binary, decimal otherwise — and, as in Python, leading zeros are then not allowed.

    perm

    math.perm(n, k): the number of ways to choose k from n in order.

    power

    pow(base, exp, modulus), and pow(base, exp) when modulus is absent.

    With a modulus and a negative exponent the result is the modular inverse raised to -exp, which is Python's behaviour since 3.8. Without a modulus a negative exponent has no integer answer and is refused.

    repr_complex

    fn repr_complex(z : Complex) -> String

    repr(z): (1+2j), 2j, (-0-1j), (nan+infj).

    repr_float

    fn repr_float(d : Double) -> String

    repr(float): the shortest string that reads back as the same double.

    repr_fraction

    fn repr_fraction(f : Fraction) -> String

    repr(Fraction(3, 4)).

    round_big

    fn round_big(d : Double) ->
    BigInt
    raise Refused

    round(d): the nearest integer, ties to even.

    round_float

    fn round_float(d : Double, ndigits~ : Int) -> Double

    round(d, ndigits): round to ndigits decimal places, ties to even, on the exact value of d.

    to_bytes

    fn to_bytes(n :
    BigInt
    , length~ : Int, big? : Bool, signed? : Bool) -> Bytes raise Refused

    int.to_bytes: length bytes holding n, big-endian unless big~ = false.

    A signed result is two's complement; an unsigned one refuses negatives.

    trunc_big

    fn trunc_big(d : Double) ->
    BigInt
    raise Refused

    math.trunc(d), as an integer.