Decimo — User Manual
Decimo v0.16.0
This page is the manual in docs/user_manual.md of the Decimo repository, at v0.16.0.
A guide to the Decimo arbitrary-precision arithmetic library for Mojo.
All code examples below assume that you have imported the prelude at the top of your Mojo file:
from decimo.prelude import *Installation
Decimo is available in the modular-community package repository. Add it to your channels list in pixi.toml:
channels = ["https://conda.modular.com/max", "https://repo.prefix.dev/modular-community", "conda-forge"]Then install:
pixi add decimoOr add it manually to pixi.toml:
decimo = ">=0.16.0, <0.17.0"Then run pixi install.
Quick Start
from decimo.prelude import *
def main() raises:
# Arbitrary-precision integer
var a = BInt("12345678901234567890")
var b = BInt(42)
print(a * b) # 518518513851851851380
print(BInt(2) ** 256) # 2^256, all 78 digits
# Arbitrary-precision decimal
var x = Decimal("123456789.123456789")
var y = Decimal("1234.56789")
print(x + y) # 123458023.691346789 (exact)
print(x.true_divide(y, precision=50)) # 50 significant digits
print(x.sqrt(precision=100)) # 100 significant digits
print(Decimal.pi(precision=1000)) # 1000 digits of πPart I — BigInt (BInt)
Overview
BigInt (alias BInt) is an arbitrary-precision signed integer type — the Mojo-native equivalent of Python's int. It supports unlimited-precision integer arithmetic, bitwise operations, and number-theoretic functions.
| Property | Value |
|---|---|
| Full name | BigInt |
| Alias | BInt |
| Internal base | 2^64 (binary representation) |
| Word type | UInt64 (little-endian) |
| Python equivalent | int |
Construction
From zero
var x = BInt() # 0From Int
var x = BInt(42)
var y = BInt(-100)
var z: BInt = 42 # Implicit conversion from IntThe constructor is marked @implicit, so Mojo can automatically convert Int to BInt where expected.
From String
var a = BInt("12345678901234567890") # Basic decimal string
var b = BInt("-98765") # Negative number
var c = BInt("1_000_000") # Underscores as separators
var d = BInt("1,234,567") # Commas as separators
var e = BInt("1.23e5") # Scientific notation (= 123000)
var f = BInt("1991_10,18") # Mixed separators (= 19911018)Note: The string must represent an integer.
BInt("1.5")raises an error. Scientific notation like"1.23e5"is accepted only if the result is an integer.
From Scalar (any integral SIMD type)
var x = BInt(UInt32(42))
var y = BInt(Int64(-5))
var z: BInt = UInt32(99) # Implicit conversionAccepts any integral scalar type (Int8 through Int256, UInt8 through UInt256, etc.).
Summary of constructors
| Constructor | Description |
|---|---|
BInt() | Zero |
BInt(value: String) | From decimal string (raises) |
BInt(value: Scalar) | From any integral scalar (implicit) |
BInt.from_integral_scalar(value) | From any integral scalar type |
BInt.from_string(value) | Explicit factory from string (raises) |
BInt.from_biguint(value, sign) | From a base-10^18 magnitude and a sign |
Unsafe constructors
These constructors skip validation for performance-sensitive code. The caller must ensure the data is valid.
| Constructor | Description |
|---|---|
BInt(uninitialized_capacity=n) | Empty words list with reserved capacity |
BInt(raw_words=Magnitude, sign=Bool) | From raw words, no leading-zero stripping |
Magnitude is BInt's word storage, exported from decimo. Its words are UInt64 and its base is 2^64; before v0.14.0 this was a List[UInt32] in base 2^32, so a literal is now [UInt64(1)], and an existing List goes in as BInt(raw_words=Magnitude(words^), sign=False).
Arithmetic Operations
Binary operators
| Expression | Description | Raises? |
|---|---|---|
a + b | Addition | No |
a - b | Subtraction | No |
a * b | Multiplication | No |
a / b | Truncating division (toward zero) | Yes (zero div) |
a // b | Floor division (rounds toward −∞) | Yes (zero div) |
a % b | Floor modulo (Python semantics) | Yes (zero div) |
divmod(a,b) | Floor quotient and remainder | Yes (zero div) |
a ** b | Exponentiation | Yes (negative exp) |
Unary operators
| Expression | Description |
|---|---|
-a | Negation |
+a | Unary plus (returns copy) |
abs(a) | Absolute value |
if a: | Nonzero test (Bool(a) is not supported) |
~a | Bitwise NOT (two's complement) |
In-place operators
+=, -=, *=, //=, %=, <<=, >>= are all supported and perform true in-place mutation to reduce memory allocation.
var a = BInt("12345678901234567890")
var b = BInt(12345)
print(a + b) # 12345678901234580235
print(a - b) # 12345678901234555545
print(a * b) # 152407406035740740602050
print(a // b) # 1000054994024671
print(a % b) # 4395
print(BInt(2) ** 10) # 1024Division Semantics
BigInt supports two division conventions:
| Name | Operator / Method | Quotient | Python equivalent |
|---|---|---|---|
| Floor division | //, %, divmod() | Toward −∞ | //, % |
| Truncate division | /, .truncate_divide(), .truncate_modulo() | Toward zero | C/Java /, % |
/ on a BInt is integer division, not a promotion to a decimal type. It follows Mojo's own Int, where Int(7) / Int(-2) is -3: the quotient stays in the type and rounds toward zero. It is therefore a different operator from //, not a synonym, and the two disagree exactly when the operands have opposite signs.
The difference matters for negative operands:
var a = BInt(7)
var b = BInt(-2)
# Floor division (Python semantics)
print(a // b) # -4
print(a % b) # -1
# Truncate division (C/Java semantics)
print(a / b) # -3
print(a.truncate_divide(b)) # -3
print(a.truncate_modulo(b)) # 1Comparison
All six comparison operators (==, !=, >, >=, <, <=) are supported. Each accepts both BInt and Int as the right operand.
var a = BInt("12345678901234567890")
print(a > 1000) # True
print(a == BInt("12345678901234567890")) # True
print(a != 0) # TrueAdditional methods:
a.compare(b) # Returns Int8: 1, 0, or -1
a.compare_magnitudes(b) # Compares |a| vs |b|Bitwise Operations
All bitwise operations follow Python's two's complement semantics for negative numbers.
| Operator | Description |
|---|---|
a & b | Bitwise AND |
a | b | Bitwise OR |
a ^ b | Bitwise XOR |
~a | Bitwise NOT: |
Each accepts both BInt and Int as the right operand. In-place variants (&=, |=, ^=) are also available.
var a = BInt(0b1100)
var b = BInt(0b1010)
print(a & b) # 8 (0b1000)
print(a | b) # 14 (0b1110)
print(a ^ b) # 6 (0b0110)
print(~a) # -13
# Negative numbers use two's complement:
print(BInt(-1) & BInt(255)) # 255Shift Operations
| Operator | Description |
|---|---|
a << n | Left shift (multiply by |
a >> n | Right shift (floor divide by |
var x = BInt(1)
print(x << 100) # 1267650600228229401496703205376 (= 2^100)
print(BInt(1024) >> 5) # 32Mathematical Functions
Exponentiation
print(BInt(2).power(100)) # 2^100
print(BInt(2) ** 100) # Same via ** operatorBoth power(exponent: Int) and power(exponent: BigInt) are supported. The exponent must be non-negative.
Integer square root
var x = BInt("100000000000000000000")
print(x.sqrt()) # 10000000000 (largest y such that y² ≤ x)
print(x.isqrt()) # Same as sqrt()Raises if the value is negative.
Number Theory
All number-theory operations are available as both instance methods and free functions:
from decimo import BInt, gcd, lcm, extended_gcd, mod_pow, mod_inverseGCD — Greatest Common Divisor
var a = BInt(48)
var b = BInt(18)
print(a.gcd(b)) # 6
print(gcd(a, b)) # 6 (free function)LCM — Least Common Multiple
print(BInt(12).lcm(BInt(18))) # 36
print(lcm(BInt(12), BInt(18))) # 36Extended GCD
Returns (g, x, y) such that a*x + b*y = g:
var result = BInt(35).extended_gcd(BInt(15))
# result = (5, 1, -2) — since 35×1 + 15×(−2) = 5Modular Exponentiation
Computes
print(BInt(2).mod_pow(BInt(100), BInt(1000000007)))
print(mod_pow(BInt(2), BInt(100), BInt(1000000007))) # free functionModular Inverse
Finds
print(BInt(3).mod_inverse(BInt(7))) # 5 (since 3×5 = 15 ≡ 1 mod 7)
print(mod_inverse(BInt(3), BInt(7))) # 5Conversion and Output
String conversions
| Method | Example output |
|---|---|
String(x) | "12345" |
repr(x) | 'BigInt("12345")' |
x.to_string_with_separators("_") | "1_234_567" |
x.to_string_with_separators(",") | "1,234,567" |
x.to_hex_string() | "0x1A2B3C" |
x.to_binary_string() | "0b110101" |
x.to_string(line_width=20) | Multi-line output |
x.to_chinese() | "十五" |
Numeric conversions
| Method | Description |
|---|---|
Int(x) | Convert to Int (raises if exceeds 64-bit range) |
Float64(x) | Convert to Float64 (may lose precision) |
x.to_biguint() | Convert the magnitude to BigUInt (base-10^18) |
var x = BInt("123456789012345678901234567890")
print(x.to_string_with_separators()) # 123_456_789_012_345_678_901_234_567_890
print(x.to_hex_string()) # 0x...to_chinese() writes the number in Chinese numerals; see Chinese Numerals for the reading rules and the available styles.
print(BInt(15).to_chinese()) # 十五
print(BInt("123456789").to_chinese()) # 一亿二千三百四十五万六千七百八十九Query Methods
| Method | Return | Description |
|---|---|---|
x.is_zero() | Bool | True if value is 0 |
x.is_negative() | Bool | True if value < 0 |
x.is_positive() | Bool | True if value > 0 |
x.is_one() | Bool | True if value is 1 |
x.bit_length() | Int | Number of bits in the magnitude |
x.bit_count() | Int | Population count (number of set bits) |
x.number_of_words() | Int | Number of UInt64 words |
x.number_of_digits() | Int | Number of decimal digits |
var x = BInt(13)
print(x.bit_length()) # 4 (13 = 0b1101)
print(x.bit_count()) # 3 (three 1-bits)
print(x.number_of_digits()) # 2
print(x.is_positive()) # TrueConstants and Factory Methods
| Method / Constant | Value |
|---|---|
BInt.zero() | 0 |
BInt.one() | 1 |
BInt.negative_one() | −1 |
BigInt.ZERO | 0 (comptime; use materialize[BigInt.ZERO]() at runtime) |
BigInt.ONE | 1 (comptime; use materialize[BigInt.ONE]() at runtime) |
BigInt.BITS_PER_WORD | 64 |
Part II — Decimal
Overview — Decimal
Decimal is an arbitrary-precision decimal type — the Mojo-native equivalent of Python's decimal.Decimal. It can represent numbers with unlimited digits and decimal places, making it suitable for financial modeling, scientific computing, and applications where floating-point errors are unacceptable.
| Property | Value |
|---|---|
| Name | Decimal |
| Aliases | BigDecimal, BDec |
| Internal base | Base-10^18 (each word stores ≤ 18 digits) |
| Default precision | 28 significant digits |
| Python equivalent | decimal.Decimal |
Decimal, BigDecimal, and BDec are all the same type. We recommend Decimal for consistency with Python's decimal.Decimal.
How Precision Works
- The default precision is 28 significant digits, matching Python's
decimalmodule. - Operators
+-*+=-=*=(and reflected__radd__/__rsub__/__rmul__) round their result HALF_EVEN to the default precision (28 significant digits), matching Pythondecimal.Decimaldefault-context arithmetic. - Methods
.add(other)/.subtract(other)/.multiply(other)(and the in-place variants.add_inplace(other)/.subtract_inplace(other)/.multiply_inplace(other)) take an optionalprecision: Int = 0argument. The defaultprecision=0returns the exact, unrounded result; passingprecision > 0rounds HALF_EVEN to that many significant digits. - Division and mathematical functions (
sqrt,ln,exp, etc.) accept an optionalprecisionparameter specifying the number of significant digits in the result.
var a = Decimal("999999999999999999999999999999") # 30 nines
var b = Decimal("1")
# Operators round to 28 digits (matches Python decimal default context)
print(a + b) # 1.000000000000000000000000000E+30
# Methods at precision=0 (default) are exact
print(a.add(b)) # 1000000000000000000000000000000
# Methods accept an explicit precision
print(a.add(b, 50)) # 1000000000000000000000000000000 (exact, fits)
print(a.multiply(a, 40)) # 40-significant-digit rounded productvar x = Decimal("2")
print(x.sqrt()) # 28 significant digits (default)
print(x.sqrt(precision=100)) # 100 significant digits
print(x.sqrt(precision=1000)) # 1000 significant digitsNote: The default precision of 28 will be configurable globally in a future version when Mojo supports global variables.
Construction — Decimal
Decimal can be constructed from various types of input using the Decimal() constructor or factory methods. Among these, the most common way is from a string representation of the decimal number, which is the most accurate way to create a Decimal without any precision loss.
From String (Decimal)
It is highly recommended to construct Decimal from a string. Please consider using this method whenever possible.
var a = Decimal("123456789.123456789") # Basic decimal string
var b = Decimal("1.23E+10") # Scientific notation
var c = Decimal("-0.000001") # Negative
var d = Decimal("1_000_000.50") # Separator supportFrom zero (Decimal)
var x = Decimal("0") # Explicitly from string
var y = Decimal() # Default constructor creates zero, same as Decimal("0")From Int (Decimal)
Although you can construct a Decimal from an Int directly, it is still risky if the Int is so large that it exceeds the maximum value of Int (which is 2^63-1).
# These work
var x = Decimal(42) # From Int
var y: Decimal = 100 # Implicit conversion (IntLiteral -> Int -> Decimal)
# This is dangerous!
var z: Decimal = 9223372036854775808 # 2^63, exceeds Int range!
print(z) # Prints -9223372036854775808, overflowed!From integral scalars
var x = Decimal(Int64(123456789))
var y = Decimal(UInt128(99999999999999))Works with all integral SIMD types. Floating-point scalars are rejected at compile time — use from_float_scalar() instead.
From floating-point — from_float_scalar()
from_float_scalar() reads the bits of the float and returns the value the float actually holds, not the literal that was written to produce it: from_float_scalar(0.1) is 0.1000000000000000055511151231257827021181583404541015625, which is what decimal.Decimal(0.1) gives as well.
Note that not all decimal numbers can be represented exactly as binary floating-point. You may lose precision without awareness.
To make the conversion from float to Decimal more explicit so that you are aware of the potential precision issues, the Decimal() constructor does not accept floating-point numbers directly. Instead, to create a Decimal from a float, you must use the from_float_scalar() factory method. It accepts any floating-point width — Float16, BFloat16, Float32, Float64 and the 8-bit formats — since every one of them widens to Float64 exactly.
Consider never using Decimal.from_float_scalar() in performance-sensitive code, but use string construction instead.
var x = Decimal.from_float_scalar(3.14159)
var y = Decimal.from_float_scalar(Float32(2.71828))from_float() is the former name and still works, forwarding unchanged. It is deprecated in favour of from_float_scalar(), which lines up with from_integral_scalar().
From Python — from_python_decimal()
You can always safely construct a Decimal from a Python decimal.Decimal using the from_python_decimal() method without worrying about precision loss.
from std.python import Python
var decimal = Python.import_module("decimal")
var py_dec = decimal.Decimal("123.456")
var a = Decimal.from_python_decimal(py_dec)
var b = Decimal(py=py_dec) # Alternative keyword-only syntaxSummary of Decimal constructors
| Constructor | Description |
|---|---|
Decimal() | Zero |
Decimal(value: String) | From string (raises) |
Decimal(value: Scalar) | From any integral scalar (impl) |
Decimal(py=py_obj) | From Python Decimal (raises) |
Decimal.from_integral_scalar(value) | From any integral scalar type |
Decimal.from_float_scalar(value) | From any float scalar (raises) |
Decimal.from_float(value) | Deprecated alias of the above |
Decimal.from_string(value) | Explicit factory from string |
Decimal.from_python_decimal(py_obj) | From Python Decimal (raises) |
Unsafe Decimal constructors
These constructors skip validation for performance-sensitive code. The caller must ensure the data is valid.
| Constructor | Description |
|---|---|
Decimal(coefficient: BigUInt, scale: Int, sign: Bool) | From raw components |
Decimal.from_raw_components(words, scale=0, sign=False) | From raw List[BigUInt.Word] words (unsafe) |
Decimal.from_raw_components(word, scale=0, sign=False) | From a single BigUInt.Word (unsafe) |
Decimal Arithmetic
The operators +, - and * round their result HALF_EVEN to 28 significant digits, as Python's decimal default context does. Use .add(), .subtract() and .multiply() for the exact, unrounded result; see How Precision Works.
| Expression | Description | Result |
|---|---|---|
a + b | Addition | Rounded to 28 digits |
a - b | Subtraction | Rounded to 28 digits |
a * b | Multiplication | Rounded to 28 digits |
a / b | True division (default precision=28) | Rounded to precision |
a // b | Truncated division (toward zero) | Integer part |
a % b | Truncated modulo | — |
a ** b | Exponentiation (default precision=28) | Rounded to precision |
divmod(a,b) | Returns (a // b, a % b) | — |
Built-in integral types are implicitly converted when used in arithmetic:
var c = Decimal("3.14") + 1 # Int → Decimal
var d = Decimal("100") * UInt(8) # UInt → DecimalIn-place operators (+=, -=, *=) perform true in-place mutation for reduced allocation.
var a = Decimal("123456789.123456789")
var b = Decimal("1234.56789")
print(a + b) # 123458023.691346789
print(a - b) # 123455554.555566789
print(a * b) # 152415787654.32099750190521Division Methods
Division is the primary operation where precision matters. Decimo provides several variants:
true_divide() — recommended for decimal division
var a = Decimal("1")
var b = Decimal("3")
print(a.true_divide(b)) # 0.3333333333333333333333333333 (28 digits)
print(a.true_divide(b, precision=50)) # 50 significant digits
print(a.true_divide(b, precision=200)) # 200 significant digitsOperator / — true division with default precision
var result = a / b # Same as a.true_divide(b, precision=28)Operator // — truncated (integer) division
print(Decimal("7") // Decimal("4")) # 1
print(Decimal("-7") // Decimal("4")) # -1 (toward zero)Decimal Comparison
All six comparison operators are supported:
var a = Decimal("123.456")
var b = Decimal("123.4560") # Same value, different scale
print(a == b) # True (comparison by value)
print(a > 100) # True (Int implicitly converted)Additional methods:
a.compare(b) # Returns Int8: 1, 0, or -1
a.compare_absolute(b) # Compares |a| vs |b|
a.max(b) # Returns the larger value
a.min(b) # Returns the smaller valueRounding and Formatting
round() — round to decimal places
var x = Decimal("123.456")
print(x.round(2)) # 123.46 (ROUND_HALF_EVEN)
print(x.round(1)) # 123.5
print(x.round(0)) # 123
print(x.round(-1)) # 1.2E+2
print(x.round(2, ROUND_DOWN)) # 123.45
print(x.round(2, ROUND_UP)) # 123.46Also works with round() builtin:
print(round(Decimal("123.456"), 2)) # 123.46quantize() — match scale of another decimal
Adjusts the scale (number of decimal places) to match the scale of exp. The actual value of exp is ignored — only its scale matters.
var x = Decimal("1.2345")
print(x.quantize(Decimal("0.01"))) # 1.23 (2 decimal places)
print(x.quantize(Decimal("0.1"))) # 1.2 (1 decimal place)
print(x.quantize(Decimal("1"))) # 1 (0 decimal places)
# Currency formatting:
var price = Decimal("19.999")
print(price.quantize(Decimal("0.01"))) # 20.00normalize() — remove trailing zeros
print(Decimal("1.2345000").normalize()) # 1.2345__ceil__, __floor__, __trunc__ {#ceil-floor-trunc}
print(Decimal("1.1").__ceil__()) # 2
print(Decimal("1.9").__floor__()) # 1
print(Decimal("-1.9").__trunc__()) # -1RoundingMode
Seven rounding modes are available:
| Constant | Description |
|---|---|
ROUND_DOWN | Truncate toward zero |
ROUND_UP | Round away from zero |
ROUND_HALF_UP | Round half away from zero (traditional) |
ROUND_HALF_DOWN | Round half toward zero |
ROUND_HALF_EVEN | Banker's rounding (default) |
ROUND_CEILING | Round toward +∞ |
ROUND_FLOOR | Round toward −∞ |
var x = Decimal("2.5")
print(x.round(0, ROUND_HALF_UP)) # 3
print(x.round(0, ROUND_HALF_EVEN)) # 2 (banker's rounding)
print(x.round(0, ROUND_DOWN)) # 2
print(x.round(0, ROUND_UP)) # 3
print(x.round(0, ROUND_CEILING)) # 3
print(x.round(0, ROUND_FLOOR)) # 2Mathematical Functions — Roots and Powers
All mathematical functions accept an optional precision parameter (default=28).
Square root
print(Decimal("2").sqrt()) # 1.414213562373095048801688724
print(Decimal("2").sqrt(precision=100)) # 100 significant digitsCube root
print(Decimal("27").cbrt()) # 3.000000000000000000000000000
print(Decimal("2").cbrt(precision=50))Nth root
print(Decimal("256").root(Decimal("8"))) # 2
print(Decimal("100").root(Decimal("3"))) # 4.641588833612778892...Power / exponentiation
print(Decimal("2").power(Decimal("10"))) # 1024
print(Decimal("2").power(Decimal("0.5"), precision=50)) # sqrt(2) to 50 digits
print(Decimal("2") ** 10) # 1024Mathematical Functions — Exponential and Logarithmic
Exponential (e^x)
print(Decimal("1").exp()) # e ≈ 2.718281828459045235360287471
print(Decimal("10").exp(precision=50)) # e^10 to 50 digitsNatural logarithm
print(Decimal("10").ln(precision=50)) # ln(10) to 50 digitsFor repeated calls, a MathCache can be used to avoid recomputing cached constants:
from decimo.bigdecimal.exponential import MathCache
var cache = MathCache()
var r1 = x1.ln(100, cache)
var r2 = x2.ln(100, cache) # Reuses cached ln(2) and ln(1.25)Logarithm with arbitrary base
print(Decimal("100").log(Decimal("10"))) # 2
print(Decimal("8").log(Decimal("2"))) # 3Base-10 logarithm
print(Decimal("1000").log10()) # 3 (exact for powers of 10)
print(Decimal("2").log10(precision=50))Mathematical Functions — Trigonometric
All trigonometric functions take inputs in radians and accept an optional precision parameter.
Basic functions
print(Decimal("0.5").sin(precision=50))
print(Decimal("0.5").cos(precision=50))
print(Decimal("0.5").tan(precision=50))Reciprocal functions
print(Decimal("1").cot(precision=50)) # cos/sin
print(Decimal("1").csc(precision=50)) # 1/sin
print(Decimal("1").sec(precision=50)) # 1/cosInverse functions
print(Decimal("1").arctan(precision=50)) # π/4 to 50 digitsA Small Argument and a Directed Mode
Each of these functions comes to a simple value as its argument goes to nought: the sine to the argument itself, the cosine to one, the cosecant to one over the argument. Every one of those is a value the destination holds exactly, and that is awkward for a function that decides its rounding rather than assuming it. Deciding means evaluating wider than asked until every value the error allows rounds the same way, and an answer that lands on a value the destination holds has values on both sides of it that round differently, however wide the evaluation goes.
The three nearest modes do not notice, since the answer is that value either way. The four directed modes do, because they have to say which side of it the truth lies on.
from decimo import Decimal, RoundingMode
var tiny = Decimal("1E-500")
print(tiny.sin(precision=20, rounding_mode=RoundingMode.ROUND_DOWN))
# 9.9999999999999999999E-501 -- the sine is below its argument
print(tiny.tan(precision=20, rounding_mode=RoundingMode.ROUND_DOWN))
# 1.0000000000000000000E-500 -- the tangent is above it
print(tiny.cos(precision=20, rounding_mode=RoundingMode.ROUND_DOWN))
# 0.99999999999999999999 -- the cosine is below oneSo such an argument is answered from the sign of the function's own first correction term, which is all the rounding needs: sin(x) = x - x^3/6 is below x and tan(x) = x + x^3/3 is above it, and neither correction can be computed at 1E-500 by any width a loop would reach.
A small argument is not the only place this happens. Wherever a function's value is a whole number, an argument a hair away from that point has an answer a hair away from a value the destination holds, and the same thing goes wrong. log10(1000) is three exactly; log10 of a thousand plus 1E-500 is three plus a correction five hundred digits down.
from decimo import Decimal, RoundingMode
var beside = Decimal("1000").add(Decimal("1E-500"))
print(beside.log10(precision=20, rounding_mode=RoundingMode.ROUND_UP))
# 3.0000000000000000001 -- above three, because the argument is above a thousand
print(beside.log10(precision=20, rounding_mode=RoundingMode.ROUND_DOWN))
# 3.0000000000000000000 -- and the same value truncated is threeBigFloat has the same answers for ln beside one, log2 and log10 beside a power of their base, and exp2 and exp10 beside a whole argument.
Mathematical Constants
π (pi)
Computed using the Chudnovsky algorithm with binary splitting:
print(Decimal.pi(precision=100)) # 100 digits of π
print(Decimal.pi(precision=1000)) # 1000 digits of πe (Euler's number)
Computed as exp(1):
print(Decimal.e(precision=100)) # 100 digits of e
print(Decimal.e(precision=1000)) # 1000 digits of eDecimal Conversion and Output
String output
The to_string() method provides flexible formatting:
var x = Decimal("123456789.123456789")
print(x) # 123456789.123456789
print(x.to_string(scientific=True)) # 1.23456789123456789E+8
print(x.to_string(engineering=True, force_exponent=True)) # 123.456789123456789E+6
print(x.to_string(delimiter="_")) # 123_456_789.123_456_789
print(x.to_string(line_width=20)) # Multi-line output
print(x.to_string(force_plain=True)) # Suppress auto-scientific notationDefault output follows CPython's Decimal.__str__() rules: plain notation when feasible, scientific notation when there would be more than 6 leading zeros or the exponent is positive.
Dec128 does not switch: it is bounded to 29 digits and a scale of 0 to 28, so plain notation is never longer than 30 characters, and it always prints plain (the .NET System.Decimal rule). The two types therefore print the same value differently below 1E-6 and at a positive exponent:
print(Decimal("1E-7")) # 1E-7
print(Dec128("1E-7")) # 0.0000001
print(Decimal("1E+28")) # 1E+28
print(Dec128("1E+28")) # 10000000000000000000000000000
print(Decimal("1E-6"), Dec128("1E-6")) # 0.000001 0.000001To get the other form: Decimal.to_string(force_plain=True) and Dec128.to_string(scientific=True).
Convenience aliases:
x.to_scientific_string() # to_string(scientific=True)
x.to_eng_string() # to_string(engineering=True)
x.to_string_with_separators("_") # to_string(delimiter="_")to_chinese() writes the number in Chinese numerals; see Chinese Numerals for the reading rules and the available styles.
print(Decimal("1050.07").to_chinese()) # 一千零五十点零七repr()
print(repr(Decimal("123.45"))) # BigDecimal("123.45")Decimal numeric conversions
var n = Int(Decimal("123.99").truncate()) # 123; Int() alone raises on a fraction
var f = Float64(Decimal("3.14")) # 3.14 (may lose precision)Decimal Query Methods
| Method | Return | Description |
|---|---|---|
x.is_zero() | Bool | True if value is zero |
x.is_one() | Bool | True if value is exactly 1 |
x.is_integer() | Bool | True if no fractional part |
x.is_negative() | Bool | True if negative |
x.is_positive() | Bool | True if strictly positive |
x.is_odd() | Bool | True if odd integer |
x.number_of_digits() | Int | Total digits in coefficient |
x.adjusted() | Int | Adjusted exponent (≈ floor(log10(|x|))) |
x.same_quantum(y) | Bool | True if both have same scale |
as_tuple() — Python-compatible decomposition
var parts = Decimal("7.25").as_tuple()
# parts[0] = False (sign), parts[1] = [7, 2, 5] (digits), parts[2] = -2 (exponent)
# sign=False, digits=[7, 2, 5], exp=-2Other methods
x.copy_abs() # Copy with positive sign
x.copy_negate() # Copy with inverted sign
x.copy_sign(other) # Copy of x with sign of other
x.fma(a, b) # Fused multiply-add: x*a+b (exact)
x.scaleb(n) # Multiply by 10^n (O(1), adjusts scale only)Python Interoperability
From Python
from std.python import Python
var decimal = Python.import_module("decimal")
var py_val = decimal.Decimal("3.14159265358979323846")
var d = Decimal.from_python_decimal(py_val)
# Or:
var d = Decimal(py=py_val)Matching Python's API
Many methods mirror Python's decimal.Decimal API:
Python Decimal method | Decimo equivalent |
|---|---|
d.quantize(exp) | x.quantize(exp) |
d.normalize() | x.normalize() |
d.as_tuple() | x.as_tuple() |
d.copy_abs() | x.copy_abs() |
d.copy_negate() | x.copy_negate() |
d.copy_sign(other) | x.copy_sign(other) |
d.fma(a, b) | x.fma(a, b) |
d.adjusted() | x.adjusted() |
d.same_quantum(other) | x.same_quantum(other) |
A note on result exponents (Decimal and Dec128)
decimo follows the preferred-exponent rules from IEEE 754-2008 §3.3 and the IBM General Decimal Arithmetic specification §4.1. Concretely:
- For
add/subtract, the result exponent ismin(exp(a), exp(b)). - For
multiply, the result exponent isexp(a) + exp(b)— even when the numerical value is zero. SoDecimal("123.45") * Decimal("0")prints as"0.00", not"0", because both operands' scales are preserved in the product's scale (-2 + 0 = -2). - For
divide, the ideal exponent isexp(a) - exp(b). The result exponent is the ideal one when the quotient is exact at that scale (Decimal("10.5") / Decimal("2.5")prints as"4.2", not"4.20"), and otherwise the smallest exponent that still represents the exact value (Decimal("123.45") / Decimal("-2")prints as"-61.725"). - The sign of zero is preserved (
Decimal("-0.00")round-trips to"-0.00").
This matches Python's decimal.Decimal, System.Decimal (.NET BCL, both C# and VB.NET), and the GDA reference. It differs from rust_decimal, which always pads multiplication-by-zero to the maximum scale of the operands and pads exact divide quotients with trailing zeros to the operand-scale difference. If you need rust_decimal-style output, call .quantize(...) or .normalize() explicitly.
Chinese Numerals
Both BInt and Decimal can write themselves in Chinese numerals with to_chinese():
print(BInt(15).to_chinese()) # 十五
print(Decimal("1050.07").to_chinese()) # 一千零五十点零七
print(Decimal("-100000001").to_chinese()) # 负一亿零一The conversion works on the decimal string of the number rather than on a particular numeric type, so it is not limited by any integer width. The string-level entry point is available directly if you need it:
from decimo.numerals import decimal_string_to_chinese
print(decimal_string_to_chinese("1050.07")) # 一千零五十点零七Reading rules
The integer part is split into sections of eight digits, counted from the decimal point. Within a section the digits take the 十/百/千 units, and the upper four take 万. Sections are joined by 亿, which multiplies everything read before it rather than labelling one section:
print(BInt("1234567890123").to_chinese())
# 一万二千三百四十五亿六千七百八十九万零一百二十三 (12345亿 + 6789万 + 123)Because 亿 is a multiplier, each further 亿 raises the magnitude by another 10^8 — 亿亿 is 10^16, 亿亿亿 is 10^24 — so arbitrarily large integers are readable without the rarely-agreed-upon 兆/京/垓 units:
print(BInt("10000000000000000").to_chinese()) # 一亿亿
print(BInt("10000000000000005").to_chinese()) # 一亿亿零五Runs of zeros collapse into a single 零, and a leading 一十 is shortened to 十 (BInt(15) is 十五, not 一十五). The fractional part is read digit by digit after 点, zeros included, so the written precision survives the conversion:
print(Decimal("1.50").to_chinese()) # 一点五零 (not 一点五)
print(Decimal("2.000").to_chinese()) # 二点零零零Styles
The character tables are supplied by ChineseNumeralStyle, which ships with four presets:
| Preset | Digits | Units | 10^4 / 10^8 | Point | Example for 1050 |
|---|---|---|---|---|---|
simplified() (default) | 一二三 | 十百千 | 万 / 亿 | 点 | 一千零五十 |
simplified_financial() | 壹贰叁 | 拾佰仟 | 万 / 亿 | 点 | 壹仟零伍拾 |
traditional() | 一二三 | 十百千 | 萬 / 億 | 點 | 一千零五十 |
traditional_financial() | 壹貳參 | 拾佰仟 | 萬 / 億 | 點 | 壹仟零伍拾 |
from decimo import ChineseNumeralStyle
print(Decimal("1050.07").to_chinese(ChineseNumeralStyle.simplified_financial()))
# 壹仟零伍拾点零柒
print(Decimal("1050.07").to_chinese(ChineseNumeralStyle.traditional()))
# 一千零五十點零七The financial (大写) styles keep the leading 一十 rather than shortening it, since dropping a digit on a cheque would make the amount easier to tamper with. A custom style can be built from the same constructor if you need a table the presets do not cover.
Digit budget
A reading is always written out in full, so its cost is set by the written length of the number, not by how compactly the value was given — Decimal("1E+1000000000") is a few characters of input and a billion digits of output. Conversions therefore take a max_digits budget, defaulting to MAX_CHINESE_NUMERAL_DIGITS (10 000), and raise a ValueError past it. The check happens before the digits are expanded, so an absurd magnitude costs nothing:
_ = Decimal("1E+1000000000").to_chinese() # raises ValueError
_ = BInt("1" + String("0") * 20000).to_chinese() # raises ValueError
# Raise or lift the cap when a very long reading really is what you want:
print(Decimal("1E+20000").to_chinese(max_digits=0))Expression Engine
eval() takes an arithmetic expression as a string and works it out with Decimal arithmetic. It is the same engine that drives the decimo CLI calculator:
from decimo import eval
print(eval("100 + e * pi"))
# 108.53973422267356706546355086954657449503488853577
print(eval("sqrt(2) + 1/3", precision=30))
# 1.74754689570642838213502205754The result is rounded to precision significant digits, 50 by default, with rounding_mode, half-even by default:
print(eval("1/3", precision=20))
# 0.33333333333333333333
print(eval("1/3", precision=5, rounding_mode=ROUND_CEILING))
# 0.33334What you can write
The operators are +, -, *, /, ^ (power), a unary minus or plus, and parentheses. A unary minus binds looser than ^, as in Python: -2^2 is -4. The constants are pi and e. The functions are sqrt, cbrt, root(x, n), ln, log(x, base), log10, exp, sin, cos, tan, cot, csc, and abs.
Names are case-sensitive: pi is the constant, PI is an unknown identifier. (The CLI lower-cases each line before it reaches the engine, which is why PI works there but not here.) Line breaks count as whitespace, so an expression can span several lines:
print(eval("""
100 + 2 * 3
""")) # 106Variables
Pass a Dict and the expression can refer to your own values. Any identifier that is not a built-in constant or function is looked up there:
from std.collections import Dict
var vars = Dict[String, Decimal]()
vars["x"] = Decimal.from_string("10")
vars["y"] = Decimal.from_string("3")
print(eval("x^2 + y", variables=vars)) # 103Errors
A syntax error, an unknown name, a division by zero, or a domain error raises, and the message says where in the expression it went wrong:
try:
_ = eval("1 / 0")
except e:
print(e) # Error at position 2: division by zeroThe individual stages
eval() tokenizes, parses to reverse Polish notation, and evaluates. The three stages are exported as well, in case you want to look at the tokens or the RPN form:
from decimo.expression import tokenize, parse_to_rpn, evaluate_rpn
var rpn = parse_to_rpn(tokenize("1 + 2 * 3"))
print(evaluate_rpn(rpn^, precision=50)) # 7evaluate() is an alias of eval().
Part III — BigFloat (BFlt)
Overview — BigFloat
BigFloat is an arbitrary-precision binary floating-point type, written in Mojo with nothing to install. Where Decimal keeps digits and a scale, BigFloat keeps a significand and a power of two, which is what makes it the right type for scientific work: a square root, an exponential or a sine has no exact decimal answer, so the question is not whether it rounds but whether it rounds correctly. Every function here does.
| Property | Value |
|---|---|
| Name | BigFloat |
| Alias | BFlt |
| Value | (-1)^sign * significand * 2^exponent |
| Significand | A BigInt, exactly precision bits when non-zero |
| Exponent | An Int, so from Int.MIN to Int.MAX |
| Precision | A field on the value, in bits, chosen per call |
| Special values | NaN, +Infinity, -Infinity, +0 and -0 |
| Rounding | All seven of decimo's modes, correctly |
| Dependencies | None |
The precision is a field on the value rather than a global context. Two values of different precisions can be added, and the destination's precision is an argument to the operation, not a setting somewhere else:
from decimo import BigFloat
from decimo.bigfloat.arithmetics import add
var a = BigFloat.from_string("0.1", 53) # 53 bits
var b = BigFloat.from_string("0.1", 200) # 200 bits
print(a == b) # False: b holds more of a tenth
print(add(a, b, 300).precision) # 300: the caller says
print((a + b).precision) # 200: an operator takes the widerConstruction — BigFloat
from decimo import BigFloat
from decimo.bigdecimal.bigdecimal import BigDecimal
var from_text = BigFloat.from_string("3.14159", 120) # rounds, 120 bits
var from_int = BigFloat.from_int(42) # exact where it fits
var from_double = BigFloat.from_float64(0.1) # exact: a double is one
var from_decimal = BigFloat.from_bigdecimal(BigDecimal("0.1"), 53)
var zero = BigFloat.zero(53, False) # a signed zero
var infinity = BigFloat.infinity(53, True) # -Infinity
var nan = BigFloat.nan(53)from_float64 is exact in both directions where there is room for it: a Float64 is a binary float of 53 bits, so at 53 bits or more nothing is lost either way and every double round-trips bit for bit. Two things qualify that. Asking for fewer bits rounds, like any other conversion -- from_float64(0.1, 24) is not 0.1. And a NaN does not survive: this type keeps one canonical NaN, so a sign or a payload goes in and does not come out.
from_string and from_bigdecimal round, because almost no decimal is a binary float, and they round correctly in whichever mode is asked for.
How Rounding Works — BigFloat
Every function takes a precision and a rounding mode, and returns the float of that precision nearest the true value, with the mode settling which side when the true value sits between two. That is a stronger promise than "accurate to the last bit or so", and the difference shows in the directed modes:
from decimo.bigfloat.exponential import sqrt
from decimo import RoundingMode
var two = BigFloat.from_int(2, 53)
print(sqrt(two, 53, RoundingMode.ROUND_DOWN)) # toward zero
print(sqrt(two, 53, RoundingMode.ROUND_UP)) # away from zero
print(sqrt(two, 53, RoundingMode.ROUND_HALF_EVEN)) # the defaultFor a transcendental function the true value cannot be computed, only approached, so the answer is decided rather than assumed: the function is evaluated wider than asked, and the rounding is only returned when every value the error allows rounds the same way. If they do not, the width grows and it is tried again. This is Ziv's method, and it is why the last bit can be trusted.
Mathematical Functions — BigFloat
| Function | Where |
|---|---|
add, subtract, multiply, divide | decimo.bigfloat.arithmetics |
sqrt, exp, ln | decimo.bigfloat.exponential |
root, cbrt | decimo.bigfloat.exponential |
hypot | decimo.bigfloat.exponential |
exp2, exp10, log2, log10, log | decimo.bigfloat.exponential |
expm1, log1p | decimo.bigfloat.exponential |
power | decimo.bigfloat.power |
sin, cos, tan | decimo.bigfloat.trigonometric |
cot, csc, sec | decimo.bigfloat.trigonometric |
arcsin, arccos, arctan | decimo.bigfloat.trigonometric |
arctan2 | decimo.bigfloat.trigonometric |
sinh, cosh, tanh | decimo.bigfloat.hyperbolic |
arcsinh, arccosh, arctanh | decimo.bigfloat.hyperbolic |
pi, ln2, ln10, e | decimo.bigfloat.constants |
fma, remainder, fmod | decimo.bigfloat.ieee |
from decimo.bigfloat.constants import pi
from decimo.bigfloat.exponential import exp, ln
from decimo.bigfloat.trigonometric import sin
print(pi(200).to_bigdecimal_rounded(60))
print(exp(BigFloat.from_int(1), 120))
print(ln(BigFloat.from_int(2), 120))
print(sin(BigFloat.from_string("1e10", 60), 53))That last one is worth a word. Reducing an argument of 10^10 by pi/2 has to cancel its leading bits against pi, so the number of bits of pi needed is counted from the argument rather than fixed — sin(2^300) fetches three hundred bits of pi before the first bit of its answer is right. An argument whose leading bit sits beyond what a precision may be is refused rather than answered badly.
Each function also has a method, which asks for this value's own precision and the default rounding; the free function is where the precision and the mode live:
print(BigFloat.from_int(8, 53).log2()) # 3
print(BigFloat.from_int(10, 53).log10()) # 1
print(BigFloat.from_int(27, 53).log(BigFloat.from_int(3, 53))) # 3The Other Bases, and the Exact Answers — BigFloat
log2, log10, log, exp2 and exp10 have something the natural exponential and logarithm do not: arguments whose answers a float holds exactly. log2(8) is three, and three is a float.
That matters because a correctly rounded transcendental function works by deciding its rounding rather than assuming it — evaluating wider than asked until every value the error allows rounds the same way. An answer sitting exactly on a representable value can never be decided that way, however wide the evaluation goes, because there are values on both sides of it that round differently. So those arguments are recognized first, from the integer significand, and answered exactly:
from decimo.bigfloat.exponential import exp2, log, log10, log2
print(log2(BigFloat.power_of_two(-70), 53)) # exactly -70
print(log10(BigFloat.from_int(1000), 53)) # exactly 3
print(log(BigFloat.from_int(27), BigFloat.from_int(3), 53)) # exactly 3
print(log(BigFloat.from_int(3), BigFloat.from_int(9), 53)) # exactly 0.5
print(exp2(BigFloat.from_int(10), 53)) # exactly 1024Which arguments those are has an exact answer in each case, and each is worth knowing:
log2(x)is exact exactly whenxis a power of two, which is every argument whose significand is a single bit, at any exponent.log10(x)is exact exactly whenxis10^kfor akat nought or above. A negative power of ten is not a binary float at all — a fifth is not a dyadic rational — solog10of the float nearest a thousandth is irrational and a hair away from-3, and it is answered by the series.log(x, base)is exact whenever the answer is exactly representable, which includeslog(3, 9) = 0.5and not only the whole answers. An answer that is exact but not representable, likelog(9, 27) = 2/3, goes through the series and comes back correctly rounded like any other.exp2(x)is exact exactly whenxis a whole number, andexp10(x)whenxis a whole number at nought or above and10^xstill fits the precision asked for. Past that10^xis not representable, so the series settles on it.expm1andlog1pare exact only at zero, where both are zero with the sign of the argument, andexpm1(-Infinity)which is exactly minus one.
A Point in the Plane — BigFloat
hypot(x, y, precision, mode) is the length of the point and arctan2(y, x, precision, mode) is its angle, with x.hypot(y) and y.arctan2(x) over them. Together they turn a pair of coordinates into polar form.
hypot forms x*x + y*y exactly, in integers, and never as two roundings that could each be wrong. Both squares are integers — a significand is one — so the sum over their common exponent is an integer too, and the integer square root of it brackets the answer with its remainder as the sticky bit. So hypot(3, 4) is 5, in every mode, which a pair of rounded squares and a rounded root could not promise.
A leg far below the other contributes nothing but a sticky bit, and the answer is then the larger leg moved by a hair — not the larger leg itself, which is the mistake that would make hypot(x, tiny) wrong in the directed modes.
arctan2 is where signed zeros earn their keep. arctan2(0, -1) is pi and arctan2(-0, -1) is -pi: the two zeros lie on opposite shores of the cut along the negative axis, and the sign is the only thing that says which one a point on the cut came from. The argument order is the one every other language uses — the second coordinate first, because the function inverts a tangent and a tangent is a rise over a run.
from decimo.bigfloat import arctan2, hypot
from decimo.bigfloat.bigfloat import BigFloat
def main() raises:
var x = BigFloat.from_int(3, 53)
var y = BigFloat.from_int(4, 53)
print(hypot(x, y, 53)) # 5
print(arctan2(y, x, 53)) # 0.92729521800161219Roots of Any Degree — BigFloat
root(x, degree, precision, mode) and cbrt(x, precision, mode), also x.root(n) and x.cbrt(), take any positive degree.
They need no series and no deciding loop, because a root is algebraic. The integer degree-th root of a scaled significand brackets the true root between two consecutive integers, and whether it landed on the lower one exactly is the sticky bit; one rounding then finishes the job in each of the seven modes. So an exact root comes out exact — root(8, 3) is 2 in every mode, including the directed ones, where a function that evaluated and then rounded would have to pick a side and both sides would be wrong.
Dividing the exponent is the whole of the scaling. x is s * 2^(q * degree + r) with r below the degree, so the factor of 2^r moves into the significand and the root sits at 2^q. The product is never formed again, which is what keeps a value near the bottom of the exponent range from leaving it.
A negative value has a real root only at an odd degree, and gives a NaN at an even one, the way sqrt does. A nought keeps its sign at an odd degree and loses it at an even one. A degree of nought or less is refused rather than answered.
The Power — BigFloat
power(x, y, precision, mode), also x.power(y) and x ** y, raises one value to another. It ends in exp(y ln x), and almost nothing about it is that line.
The reason is the same one the other bases have, only more so. A power sits on an exactly representable answer constantly: 2 ** 10 is 1024, which every directed mode has to return and no bracket around it can prove. What is less obvious is that a fractional exponent does it too — 4 ** 0.5 is exactly 2, and 16 ** 0.25 is as well — so recognizing the whole exponents is not enough.
Three kinds of exact answer are found before any series runs.
A whole exponent is a power of the significand. x is s * 2^e, so x^n is s^n * 2^(en), and s^n is an integer however large; it is formed while it is short enough to be worth forming, and one rounding finishes it. A negative whole exponent is one correctly rounded division of exact operands.
A base that is a power of two answers any exponent leaving a whole result: 2^m to the a / 2^b is 2^(ma / 2^b), exact exactly when 2^b divides m.
Anything else with a fractional exponent — and every binary float is a / 2^b for some a and b — is b square roots followed by a whole power. A square root is exact only when the significand is a perfect square, and each exact root halves the bits, so the question answers itself within a few steps or not at all.
What reaches the series is therefore known to be irrational, where the loop settles.
from decimo.bigfloat import power
from decimo.bigfloat.bigfloat import BigFloat
from decimo.rounding_mode import RoundingMode
def main() raises:
var two = BigFloat.from_int(2, 53)
print(two ** BigFloat.from_int(10, 53)) # 1024, in every mode
print(BigFloat.from_int(4, 53) ** BigFloat.from_string("0.5", 53)) # 2
print(power(two, BigFloat.from_string("0.5", 53), 53,
RoundingMode.ROUND_DOWN)) # 1.4142135623730949The special values follow IEEE 754's pow, which answers two cases before it looks at anything else: a nought exponent gives one for every base, a NaN included, and a base of one gives one for every exponent, a NaN included. A negative base with a fractional exponent gives a NaN, there being no real answer. There is no overflow to an infinity for finite arguments — this type's exponent is as wide as an Int, and an answer asking for more is refused rather than answered with an infinity it did not earn.
Holding On Near Zero — BigFloat
expm1 is exp(x) - 1 and log1p is ln(1 + x), and both exist because the expressions they are named after throw their answers away for a small argument. exp(2^-100) rounds to one, so the difference is nought where the answer is 2^-100; 1 + 2^-100 rounds to one, so the logarithm is nought too. These two compute the difference and the logarithm directly:
from decimo.bigfloat.exponential import expm1, log1p
var tiny = BigFloat.power_of_two(-100000)
print(expm1(tiny, 53).internal_representation()) # 2^-100000, not nought
print(log1p(tiny, 53).internal_representation()) # and a hair under itThey are also what the hyperbolic functions are built on, which is why sinh(2^-100000) is 2^-100000 rather than zero.
Special Values — BigFloat
The special values follow IEEE 754 rather than Decimal's rules, because this is a float:
from decimo.bigfloat.trigonometric import arctan
print(sqrt(BigFloat.from_int(-4), 53).is_nan()) # True, not an error
print(ln(BigFloat.zero(), 53)) # -Infinity
print(sin(BigFloat.infinity(), 53).is_nan()) # True: it settles nowhere
print(arctan(BigFloat.infinity(), 53)) # pi/2: this one settlesA NaN is unordered, so every comparison with it is False except !=, and that holds for a NaN against itself. The two zeros compare equal. Sorting therefore needs an order that covers them, and compare_total() is it: it runs from -Infinity up to the NaN and separates values that are numerically equal by their precision.
There is no overflow to an infinity for a finite argument. The exponent is as wide as an Int, so an operation that asks for more — exp of anything from about 6.4e18 up — raises rather than returning an infinity it did not earn.
The IEEE 754 Companions — BigFloat
Beyond the arithmetic and the functions, IEEE 754 asks a format for a set of small operations that numerical code reaches for constantly. They are in decimo.bigfloat.ieee, which is where they are imported from, and each one is also a method on the value:
| Function | What it gives |
|---|---|
next_plus(x, p) | The smallest value above x at p bits |
next_minus(x, p) | The largest value below it |
next_toward(x, y, p) | One step from x in the direction of y |
logb(x) | floor(log2(abs(x))), as a BigInt |
scaleb(x, n) | x * 2^n, exactly |
copy_sign(x, y) | x with the sign of y |
copy_abs(x), copy_negate(x) | The sign dropped, the sign flipped |
number_class(x) | +Normal, -Zero, NaN, and so on |
is_integer(x) | Whether x is a whole number |
truncate(x), floor(x), ceil(x) | The three named roundings to a whole number |
round_to_integer(x, mode) | The same in any of the seven modes |
fma(x, y, z, p, mode) | x * y + z with a single rounding |
remainder(x, y, p) | x - y*n, n the nearest integer |
fmod(x, y, p) | x - y*n, n the quotient truncated |
from decimo import BigFloat
from decimo.bigfloat.ieee import fma, next_plus, remainder
var one = BigFloat.from_float64(1.0)
print(next_plus(one, 53)) # 1 + 2^-52
print(one.next_minus(53)) # 1 - 2^-53, half a step away
print(String(BigFloat.from_float64(12.0).logb())) # 3
print(one.scaleb(10)) # 1024, nothing computed
print(BigFloat.from_string("-0.5", 53).ceil()) # -0, the sign surviving
print(remainder(BigFloat.from_float64(5.0), BigFloat.from_float64(3.0), 53))
# -1: six is nearer to five than three is. `fmod` of the same pair is 2.fma is the one with substance. x * y + z written out rounds the product before the addition sees it, and a product that nearly cancels with z loses exactly the bits the answer is made of. Here the product is formed exactly — it is an integer times a power of two, so there is nothing to round — and only the sum is rounded:
var above = next_plus(one, 53) # 1 + 2^-52
var product = above * above # rounded: 1 + 2^-51
print(product + (-product)) # 0: the two-step answer
print(fma(above, above, -product, 53)) # 2^-104: what the rounding droppedTwo of these differ from a double, and both follow from the type rather than from a choice. number_class never answers +Subnormal or -Subnormal: a significand here always holds its full precision, so every finite non-zero value is normal. And next_plus and next_minus step through an exponent range as wide as an Int, so the ends they reach are not a double's — next_plus(BigFloat.zero(), 53) is 2^(Int.MIN + 52) where a double answers 2^-1074. Within that range the two agree bit for bit, which is how these are tested.
logb returns a BigInt rather than an Int because the position of the leading bit is exponent + precision - 1, and that sum can be outside an Int while the value itself is an ordinary one. It has no answer for a zero, an infinity or a NaN, where the standard's answers are infinities and a NaN, and raises instead — as the decimal logb() does.
Decimal In, Decimal Out — BigFloat
Every binary float is exactly a decimal, because 2^-k is 5^k / 10^k, so the conversion out loses nothing and only the number of digits is a choice:
var tenth = BigFloat.from_float64(0.1)
print(tenth.to_bigdecimal()) # all 55 digits of the double
print(tenth.to_bigdecimal_rounded(17)) # 0.10000000000000001
print(tenth) # as many digits as 53 bits are worth
print(tenth.internal_representation()) # the significand, exponent, precisionA digit count costs what the digits cost. The digits asked for are taken by scaling the value by a power of ten, so a deep exponent is no longer a reason to wait: a value whose exact expansion runs to 301,031 digits still gives twenty of them at once, and the smallest and largest values a BigFloat can hold print like any other.
var deep = BigFloat.power_of_two(-1000000)
print(deep.to_string(20)) # 1.0100340591980302247E-301030to_bigdecimal() is the one to ask when every digit is wanted, and it is the only one whose cost follows the exponent, because the answer does.
Coming in costs what the precision is, in the same way and for the same reason. A decimal exponent of nine or eighteen figures is a decimal nobody could write out, and it parses at once:
var deep = BigFloat.from_string("1.5e-1000000", 53)
print(deep.to_string(17)) # 1.5000000000000000E-1000000
print(BigFloat.from_string("7e1000000000", 53).to_string(17))
# 6.9999999999999999E+1000000000The second one is seven to a billion powers of ten, which has no exact rendering in 53 bits -- the digits shown are the float's own.
Going the other way rounds. to_float64() is exact for a normal double but rounds a second time when the answer is subnormal, since a subnormal has fewer bits than 53: converting from a wider precision avoids it.
BigFloat, Decimal, or MPF
Use Decimal when the arithmetic has to be decimal — money, anything whose digits are the point, anything that has to agree with Python's decimal.Decimal. Use BigFloat for scientific computation at a chosen precision, where binary is the natural base and the question is whether the last bit is right.
MPF, the MPFR-backed float that used to carry the name BigFloat, is still in the source at decimo.mpf.mpf.MPF. It is not exported and carries no promise of a stable interface, and using it means installing MPFR and linking a C wrapper. It is kept as a second implementation to test the traits against, as the baseline the Mojo one is measured against, and for anyone who was relying on it.
Part IV — Rational
An exact fraction of two arbitrary-precision integers. Nothing is ever rounded: a third is a third, and a third times three is one.
Rational is not in the prelude, which carries the four types most programs reach for. Import it by name, as itself or under the short name Frac:
from decimo import Frac, Rational
from decimo.bigint.bigint import BigIntThe value is always in lowest terms with a positive denominator, so two equal values have the same two parts and == is a comparison of integers rather than of fractions.
Construction — Rational
print(Rational(3, 4)) # 3/4
print(Rational(BigInt(6), BigInt(8))) # 3/4, reduced on the way in
print(Rational("22/7")) # 22/7
print(Rational("0.25")) # 1/4, a decimal literal
print(Rational.from_float_scalar(0.1)) # 3602879701896397/36028797018963968The float constructor is exact and that is the point of it: a double holding "0.1" is not a tenth, and this is the fraction it really holds. Use Rational("0.1") for the tenth.
Arithmetic — Rational
The four operations, the comparisons and the in-place forms are all exact. Multiplication and division reduce across the two fractions before multiplying, so the intermediates stay near the size of the answer rather than growing to the product of the denominators.
print(Rational(1, 2) + Rational(1, 3)) # 5/6
print(Rational(1, 2) / Rational(1, 3)) # 3/2
print(Rational(1, 3) < Rational(1, 2)) # True
var x = Rational(1, 2)
x += Rational(1, 3) # 5/6
x **= 3 # 125/216A power takes a whole exponent, which may be negative:
print(Rational(3, 2) ** 7) # 2187/128
print(Rational(3, 2) ** -2) # 4/9A fractional exponent is not accepted. Python's Fraction answers one with a float, which throws away the exactness the type exists for; ask BigFloat for that answer instead, where the precision is yours to choose.
Rounding — Rational
The four roundings all return a Rational with a denominator of one, and the standard library's functions take this type:
from std.math import ceil, floor, trunc
var half_down = Rational(-7, 2)
print(floor(half_down)) # -4
print(ceil(half_down)) # -3
print(trunc(half_down)) # -3, toward zero
print(round(half_down)) # -4
print(round(Rational(5, 2))) # 2, halves go to the even side
print(round(Rational(1, 8), 2)) # 3/25, which is 0.12round(x, n) takes a negative n to round above the decimal point, as Python's does: round(Rational(1350), -2) is 1400.
The Closest Fraction Under a Limit — Rational
limit_denominator answers the closest fraction whose denominator is at most the limit, which is how a long exact value becomes a short readable one:
var pi_ish = Rational(BigInt(31415926535), BigInt(10000000000))
print(pi_ish.limit_denominator(BigInt(10))) # 22/7
print(pi_ish.limit_denominator(BigInt(1000))) # 355/113It walks the continued fraction of the value, so the work is proportional to the length of that walk and not to the size of the denominator it starts from. Where two fractions are equally close it returns the same one Python's Fraction does, which is not always the one with the smaller denominator.
Conversions — Rational
print(Rational(1, 3).to_float()) # 0.3333333333333333
print(Rational(1, 3).to_bigdecimal(20)) # 0.33333333333333333333
print(Rational(-7, 2).to_integer()) # -3, truncated toward zero
print(Rational(-7, 2).reciprocal()) # -2/7to_bigdecimal takes the digits to keep, because a third has no decimal expansion that ends. Everything else here is exact.
Appendix A — Import Paths
# Recommended: import everything commonly needed
from decimo.prelude import *
# Brings in: BigInt, BInt, Decimal, BigDecimal, BDec, Decimal128, Dec128,
# BigFloat, BFlt, RoundingMode, ROUND_DOWN, ROUND_HALF_UP,
# ROUND_HALF_EVEN, ROUND_UP, ROUND_CEILING, ROUND_FLOOR
# Or import specific types
from decimo import BInt, BigInt
from decimo import Decimal # also available as BigDecimal or BDec
from decimo import BigFloat, BFlt
from decimo import RoundingMode
# The exact fraction is exported but not in the prelude, which carries the
# four types most programs reach for.
from decimo import Rational
# The MPFR-backed float is in the source but not exported, and carries no
# promise of a stable interface. It also needs its C wrapper linked; see
# `src/decimo/mpf/__init__.mojo`.
from decimo.mpf.mpf import MPF
# Number-theory free functions
from decimo import gcd, lcm, extended_gcd, mod_pow, mod_inverse
# The binary float's own functions live in their modules; the top
# level exports the type and not its functions
from decimo.bigfloat import exp, exp2, exp10, expm1, ln, ln10, log
from decimo.bigfloat import log1p, log2, log10, sqrt
from decimo.bigfloat import power
from decimo.bigfloat import cbrt, root
from decimo.bigfloat import arctan2, hypot
from decimo.bigfloat import ceil, floor, number_class, truncate
from decimo.bigfloat import fma, logb, next_plus, remainder, scaleb
from decimo.bigfloat.trigonometric import cos, sin, tan
# Chinese numerals: the style presets are re-exported at the top level, the
# string-level engine lives in the `decimo.numerals` sub-package
from decimo import ChineseNumeralStyle
from decimo.numerals import (
decimal_string_to_chinese,
MAX_CHINESE_NUMERAL_DIGITS,
)
# Expression engine: `eval` at the top level, the individual stages in the
# `decimo.expression` sub-package
from decimo import eval
from decimo.expression import tokenize, parse_to_rpn, evaluate_rpnAppendix B — Traits Implemented
All of these come from the Mojo standard library except Numeric, Parsable and Rootable, which Decimo defines in decimo.traits. BigInt, BigDecimal and Decimal128 conform to all three. Mojo checks conformance nominally, which is why the traits live in Decimo rather than in the consumer.
Numeric gathers zero(), one(), -x, +, -, * and /, so a generic routine — a matrix or polynomial library, say — can be written once against T: Numeric and run over any of the three.
Parsable requires the static from_string(value), so the same generic routine can also fill itself from text. The two are separate because the capabilities are: the MPFR-backed MPF, which is in the source but outside the public interface, parses but is Movable without being Copyable, so it can never be Numeric. Ask for T: Numeric & Parsable to get both.
Rootable requires sqrt(), the one operation a Cholesky or QR factorisation needs beyond arithmetic. It is separate for the same reason, and here the evidence is sharper: BigUInt has a square root but is unsigned, so it has no __neg__ and can never be Numeric either. Its supertraits are Deinitable and Movable and no more — Copyable is pointedly absent, so that MPF can conform. BigFloat, MPF and BigUInt therefore conform to Rootable as well, six types in all. Ask for T: Numeric & Rootable in a routine that needs both. What the root means stays the implementing type's business: on an integral type it truncates, so BigInt("10").sqrt() is 3, exactly as / truncates there. So does what a negative value does — the four exact types raise, while BigFloat and MPF return a NaN, as they do for every other function outside their domain.
Hashable gives hash(x), and with it a key in a Dict. BigUInt, BigInt, BigDecimal, BigFloat, Rational and Decimal128 all conform. The rule a hash has to keep is that equal values hash alike, and each of these types holds values that are equal without being written the same way, so each has something to see through. A decimal drops the trailing zeros it was written with, which is why 1.0 and 1.00 reach the same entry. A binary float shifts its significand down to an odd number, which is why a half at fifty-three bits and a half at five hundred reach the same entry. A zero is one key whatever sign it carries. The converse is not promised: two different numbers may land on the same sixty-four bits, as they may under any hash, and the dictionary settles that with ==.
Every one of these types also answers String(x) and repr(x). Neither needs a trait of its own here: String(x) and print(x) come from Writable, and repr(x) from the type's own __repr__. Mojo 1.1 has no Stringable or Representable to ask for, so generic code that wants to print its argument asks for Writable.
BigInt
| Trait | What it enables |
|---|---|
Absable | abs(x) |
Comparable | <, <=, >, >=, ==, != |
Copyable | Value-semantic copy |
Movable | Move semantics |
FloatableRaising | Float64(x) |
Hashable | hash(x), a key in a dictionary |
IntableRaising | Int(x) |
Numeric | Generic code over Decimo numbers |
Parsable | T.from_string(text) in generic code |
Rootable | x.sqrt() in generic code |
Writable | print(x), String(x) |
BigDecimal
| Trait | What it enables |
|---|---|
Absable | abs(x) |
Comparable | <, <=, >, >=, ==, != |
Copyable | Value-semantic copy |
Movable | Move semantics |
FloatableRaising | Float64(x) |
Hashable | hash(x), a key in a dictionary |
IntableRaising | Int(x) |
Numeric | Generic code over Decimo numbers |
Parsable | T.from_string(text) in generic code |
Rootable | x.sqrt() in generic code |
Roundable | round(x), round(x, ndigits) |
Writable | print(x), String(x) |
Decimal128
| Trait | What it enables |
|---|---|
Absable | abs(x) |
Boolable | Bool(x), if x |
Comparable | <, <=, >, >=, ==, != |
Floatable | Float64(x), which cannot overflow |
Hashable | hash(x), a key in a dictionary |
IntableRaising | Int(x) |
Numeric | Generic code over Decimo numbers |
Parsable | T.from_string(text) in generic code |
Rootable | x.sqrt() in generic code |
Roundable | round(x), round(x, ndigits) |
TrivialRegisterPassable | Passed in registers, copied bitwise |
Writable | print(x), String(x) |
BigFloat
| Trait | What it enables |
|---|---|
Absable | abs(x) |
Comparable | <, <=, >, >=, ==, != |
Copyable | Value-semantic copy |
Hashable | hash(x), a key in a dictionary |
Movable | Move semantics |
Parsable | T.from_string(text) in generic code |
Rootable | x.sqrt() in generic code |
Writable | print(x), String(x) |
Parsable asks for a from_string(text) of one argument, and a binary float has to be told how many bits to keep, so there are two: the one the trait needs, which lands at the default precision of fifty-three bits, and from_string(text, precision, rounding_mode) for everything else. Generic code bounded on T: Parsable therefore gets a float at a double's precision, which is the only precision it could have asked for without saying so.
Comparable is conformed to with the one reservation IEEE 754 asks for: a NaN is unordered, so <, <=, >, >= and == are all False for it and != is True, which breaks the law that exactly one of less, equal and greater holds. compare_total() is there for code that needs a total order.
Numeric is absent for now: it asks for operators that round, and this type's operators have to be told a precision to round to. The free functions in decimo.bigfloat.arithmetics take one.
Appendix C — Complete API Tables
BigFloat — All Functions
Every one of these takes a precision in bits and a rounding mode, and returns the float of that precision nearest the true value.
| Function | Module |
|---|---|
add, subtract | decimo.bigfloat.arithmetics |
multiply, divide | decimo.bigfloat.arithmetics |
sqrt | decimo.bigfloat.exponential |
root, cbrt | decimo.bigfloat.exponential |
hypot | decimo.bigfloat.exponential |
exp, ln | decimo.bigfloat.exponential |
exp2, exp10 | decimo.bigfloat.exponential |
log2, log10, log | decimo.bigfloat.exponential |
expm1, log1p | decimo.bigfloat.exponential |
power | decimo.bigfloat.power |
sin, cos, tan | decimo.bigfloat.trigonometric |
cot, csc, sec | decimo.bigfloat.trigonometric |
arcsin, arccos, arctan | decimo.bigfloat.trigonometric |
arctan2 | decimo.bigfloat.trigonometric |
sinh, cosh, tanh | decimo.bigfloat.hyperbolic |
arcsinh, arccosh, arctanh | decimo.bigfloat.hyperbolic |
pi, ln2, ln10, e | decimo.bigfloat.constants |
fma, remainder, fmod | decimo.bigfloat.ieee |
log takes the base as its second argument, before the precision. Every other one takes the argument, the precision and the mode.
cot and csc have a pole at zero and return an infinity of the zero's sign, the way a float does; BigDecimal raises there instead, having no infinity to return. No other pole of the six is a binary float, so every other argument has an answer, however large it is.
These take no rounding mode, or no precision, because their answers are exact or carry the argument's own precision. All of them are in decimo.bigfloat.ieee, and all of them are methods as well.
| Function | Description |
|---|---|
next_plus, next_minus | The neighbour at a given precision |
next_toward | The neighbour in another value's direction |
logb | The leading bit's position, as a BigInt |
scaleb | Times a power of two, exactly |
copy_sign, copy_abs, copy_negate | The sign, never raising |
number_class | The specification's name for the value |
is_integer | Whether there is a fractional part |
truncate, floor, ceil | The three named roundings to a whole number |
round_to_integer | The same in any of the seven modes |
| Operator / Method | Description |
|---|---|
a + b, a - b | At the wider precision, half to even |
a * b, a / b | The same |
-a, abs(a) | Exact |
a == b, a < b, and the rest | False for a NaN, both zeros equal |
a.sqrt() | At a's own precision, half to even |
a.fma(b, c, p) | a * b + c, the product formed exactly |
a.remainder(b, p), a.fmod(b, p) | The two remainders |
a.next_plus(p), a.next_minus(p) | The neighbours |
a.logb(), a.scaleb(n) | The exponent, read and written |
a.truncate(), a.floor(), a.ceil() | Rounded to a whole number |
a.round_to_integer(mode) | The same in any mode |
a.number_class(), a.is_integer() | What kind of value it is |
a.exp(), a.ln() | The same |
a.exp2(), a.exp10() | The same; a whole exponent is exact |
a.log2(), a.log10() | The same; a whole power of the base is exact |
a.log(base) | At the wider precision, half to even |
a.expm1(), a.log1p() | The same; these keep a small argument |
a.to_bigdecimal() | The exact decimal expansion |
a.to_bigdecimal_rounded(n) | n significant digits |
a.to_float64() | The nearest double |
a.internal_representation() | The significand, exponent and precision |
BigInt — All Operators
| Operator / Method | Accepts | Raises? | Description |
|---|---|---|---|
a + b | BInt, Int | No | Addition |
a - b | BInt, Int | No | Subtraction |
a * b | BInt, Int | No | Multiplication |
a / b | BInt, Int | Yes | Truncating division |
a // b | BInt, Int | Yes | Floor division |
a % b | BInt, Int | Yes | Floor modulo |
a ** b | BInt, Int | Yes | Power |
a << n | Int | No | Left shift |
a >> n | Int | No | Right shift |
a & b | BInt, Int | No | Bitwise AND |
a | b | BInt, Int | No | Bitwise OR |
a ^ b | BInt, Int | No | Bitwise XOR |
~a | — | No | Bitwise NOT |
-a | — | No | Negation |
abs(a) | — | No | Absolute value |
a.sqrt() | — | Yes | Integer square root |
a.gcd(b) | BInt | No | GCD |
a.lcm(b) | BInt | Yes | LCM |
a.extended_gcd(b) | BInt | Yes | Extended GCD |
a.mod_pow(e, m) | BInt/Int, BInt | Yes | Modular exponentiation |
a.mod_inverse(m) | BInt | Yes | Modular inverse |
Decimal — Mathematical Functions
| Function | Signature | Default | Description |
|---|---|---|---|
sqrt | x.sqrt(precision=28) | 28 | Square root |
cbrt | x.cbrt(precision=28) | 28 | Cube root |
root | x.root(n, precision=28) | 28 | Nth root |
power | x.power(exp, precision=28) | 28 | Exponentiation |
exp | x.exp(precision=28) | 28 | e^x |
ln | x.ln(precision=28) | 28 | Natural logarithm |
log | x.log(base, precision=28) | 28 | Logarithm (any base) |
log10 | x.log10(precision=28) | 28 | Base-10 logarithm |
sin | x.sin(precision=28) | 28 | Sine (radians) |
cos | x.cos(precision=28) | 28 | Cosine (radians) |
tan | x.tan(precision=28) | 28 | Tangent (radians) |
cot | x.cot(precision=28) | 28 | Cotangent (radians) |
csc | x.csc(precision=28) | 28 | Cosecant (radians) |
sec | x.sec(precision=28) | 28 | Secant (radians) |
arctan | x.arctan(precision=28) | 28 | Arctangent (radians) |
pi | Decimal.pi(precision) | — | Compute π |
e | Decimal.e(precision) | — | Compute e |