finanza/decimal
Fixed-point decimal arithmetic with explicit rounding.
A Decimal is represented internally as a signed
integer coefficient and an exponent:
value = coefficient × 10^exponent
Decimal is pub opaque; construct values through from_int,
from_string, or new, and inspect them through
coefficient and exponent.
Precision boundary
On the JavaScript target, Gleam’s Int is a 64-bit IEEE 754 number,
so coefficients are limited to ±(2^53 − 1) = 9_007_199_254_740_991.
Operations that would produce a larger coefficient return
PrecisionExceeded. On the Erlang target,
integers are arbitrary precision; the same bound is enforced anyway
so behaviour is consistent across targets.
Types
Errors returned by arithmetic operations.
pub type ArithmeticError {
DivisionByZero
PrecisionExceeded
}
Constructors
Errors returned by validated constructors
(try_new, try_from_int).
pub type ConstructError {
CoefficientTooLarge
}
Constructors
-
CoefficientTooLargeThe supplied coefficient — or the value implied once the exponent is applied (
|coefficient| × 10^exponentforexponent ≥ 0) — would exceed±9_007_199_254_740_991(the JavaScript-safe integer ceiling). Such a value cannot round-trip throughto_stringandfrom_string, so construction fails fast rather than producing aDecimalthe library cannot read back.
Fixed-point decimal value. Construct via from_int,
from_string, or new.
pub opaque type Decimal
Errors returned by format_checked when the
thousands or decimal separator arguments would produce an
ambiguous or un-parseable rendering.
pub type FormatError {
MultiCharSeparator(field: String, value: String)
SeparatorsCollide(value: String)
EmptyDecimalSeparator
}
Constructors
-
MultiCharSeparator(field: String, value: String)A separator argument has more than one grapheme. The
fieldis either"thousands"or"decimal";valueis the offending input echoed back so the caller can route the failure to its locale layer. -
SeparatorsCollide(value: String)The
thousandsanddecimal_separatorarguments are equal (and non-empty). The rendered output would contain the same character at every separator position and parsers could not disambiguate the integer / fractional split. -
EmptyDecimalSeparatorThe
decimal_separatorargument is the empty string. The rendered output would lose the integer / fractional split entirely ("1234.5"would format as"1234,5").
Errors returned by from_string.
pub type ParseError {
EmptyInput
InvalidCharacter(char: String, position: Int)
MultipleDecimalPoints
MultipleSigns
NoDigits
ParsedValueTooLarge
}
Constructors
-
EmptyInputThe input was the empty string or contained only whitespace.
-
InvalidCharacter(char: String, position: Int)The input contained a character that is not a digit, sign, or decimal point.
-
MultipleDecimalPointsThe input contained more than one decimal point.
-
MultipleSignsThe input contained more than one sign character.
-
NoDigitsThe input contained no digits (e.g.
"+",".","-."). -
ParsedValueTooLargeThe parsed coefficient would exceed
±9_007_199_254_740_991(the JavaScript-safe integer ceiling). Such a value cannot be represented faithfully on the JavaScript target; rather than silently corrupt it (and emit unparseable strings fromto_string), parsing fails fast.
Values
pub fn add(
a a: Decimal,
b b: Decimal,
) -> Result(Decimal, ArithmeticError)
Add two decimals.
When one operand is zero the result is just the other operand —
we short-circuit without going through align. Without the
short-circuit, align would try to scale the smaller-exponent
operand up to the larger’s exponent (e.g. 1 × 10^20 for
new(1, 20) + zero()), which can exceed max_safe_coefficient
and surface a spurious PrecisionExceeded despite the
mathematical result fitting trivially. When both operands are
zero we still return zero, but with the smaller of the two
exponents so that add(a, b) == add(b, a) holds at the level of
structural equality (the same invariant align provided before).
pub fn compare(a a: Decimal, b b: Decimal) -> order.Order
Total ordering. Two values with the same numeric value compare as equal even when their exponents differ.
pub fn divide(
a a: Decimal,
b b: Decimal,
digits digits: Int,
mode mode: rounding.Mode,
) -> Result(Decimal, ArithmeticError)
Divide a by b, returning a result rounded to digits decimal
places using mode.
Returns DivisionByZero when b is zero, or
PrecisionExceeded when the intermediate
representation would exceed ±9_007_199_254_740_991 (the
JavaScript-safe integer ceiling). See
max_safe_digits for the practical upper
bound on digits: requests above that value will exceed the
precision window even when both operands are unit-magnitude,
and larger operands shrink the headroom further. For the
per-call boundary the rule of thumb is
`digits ≤ floor(log10(max_safe_coefficient / abs(a.coefficient)))
- (b.exponent − a.exponent)
. Callers needing more precision should reach for a dedicated arbitrary-precision library (Pythondecimalwithprec=50+`, etc.).
pub fn equal(a a: Decimal, b b: Decimal) -> Bool
Equality test by numeric value, not by representation.
equal(new(coefficient: 100, exponent: -2), one()) is True.
pub fn format(
d d: Decimal,
thousands thousands: String,
decimal_separator decimal_separator: String,
) -> String
Render a Decimal with custom thousands and decimal separators.
format(d, thousands: ",", decimal_separator: ".") // "1,234.56"
format(d, thousands: ".", decimal_separator: ",") // "1.234,56" (German)
format(d, thousands: "", decimal_separator: ".") // "1234.56"
Equivalent to to_string when thousands is empty
and decimal_separator is ".".
pub fn format_checked(
d d: Decimal,
thousands thousands: String,
decimal_separator decimal_separator: String,
) -> Result(String, FormatError)
Like format, but validates the separator arguments
against the locale-formatter contract and returns the failure as
a Result instead of producing a garbled rendering. The checks
catch the three configuration mistakes that format would
otherwise swallow:
thousandsordecimal_separatorwith more than one grapheme (the function is a single-character separator formatter; longer inputs almost always indicate a config-pipeline bug)- identical
thousandsanddecimal_separator(the output would render the same character at every separator position and be un-parseable round-trip) - empty
decimal_separator(the output would lose the integer / fractional split entirely)
thousands empty is still accepted and yields “no grouping” —
the standard contract for to_string. Use this entry point when
the separator arguments come from configuration, locale data, or
any other dynamic source where surfacing the failure as a value
matters; keep using format when the arguments are call-site
literals you control.
pub fn from_float(
value value: Float,
) -> Result(Decimal, ParseError)
Build a Decimal from a Float.
The conversion goes through float.to_string |> from_string,
which means the resulting Decimal matches Gleam’s textual
rendering of the float. That rendering is the same one
gleam_stdlib uses for string.inspect(value) and is generally
the “shortest IEEE-754 round-trip” form on both targets — so
inputs like 0.5 and 3.14 survive intact, while pathological
floats (0.1 +. 0.2 → 0.30000000000000004) carry their full
double expansion into the resulting Decimal.
Returns the same ParseError variants from_string does so a
runtime float that for any reason cannot be parsed (NaN /
Infinity surface as non-numeric strings on Erlang) propagates as
Error(_) rather than panicking.
Use this when the input genuinely is a Float (exchange-rate
APIs, telemetry); prefer from_string or try_new when the
caller already holds a textual or coefficient-and-exponent
representation, since those skip the float round-trip and stay
target-portable.
pub fn from_int(n n: Int) -> Decimal
Build a Decimal from an integer.
Panics if |n| > 9_007_199_254_740_991 (such a coefficient
cannot round-trip through to_string and
from_string). Use try_from_int
when the input is supplied by a caller and might exceed the
safe range — that variant returns a Result instead of
panicking.
pub fn from_string(
input input: String,
) -> Result(Decimal, ParseError)
Parse a decimal from a string. Accepts an optional leading + or
-, decimal digits, and at most one . separator. Scientific
notation (1e10, 1.5E+2, 1e-10) is also accepted: the mantissa
is parsed by the same rules and the trailing [eE][+-]?digits
shifts the resulting Decimal’s exponent. This matches
dataprep/parse.float’s convention so values produced by Float
→ String round-trips on either target flow into decimal
without an intermediate string.replace step.
from_string("3.14") // Ok(Decimal with coefficient=314, exponent=-2)
from_string("-0.5") // Ok(Decimal with coefficient=-5, exponent=-1)
from_string("1e10") // Ok(Decimal with coefficient=1, exponent=10)
from_string("1.5e+2") // Ok(Decimal with coefficient=15, exponent=1)
from_string("1e-10") // Ok(Decimal with coefficient=1, exponent=-10)
from_string("") // Error(EmptyInput)
from_string("1.2.3") // Error(MultipleDecimalPoints)
pub const max_safe_digits: Int
Upper bound on digits arguments to divide (and the
other operations that scale by 10^digits) when both operands are
unit-magnitude. max_safe_coefficient is ≈ 9 × 10^15, so
10^16 > max_safe_coefficient and division to 16 fractional
digits already exceeds the safe range even for 1/1. Larger
operands shrink the practical headroom further; treat
max_safe_digits as the best case ceiling, not a guarantee.
For any specific division, the safe digits value is bounded by
floor(log10(max_safe_coefficient / abs(a.coefficient))) plus
b.exponent − a.exponent. Callers that need more precision should
reach for a dedicated arbitrary-precision library (e.g. Python
decimal with prec=50+).
pub fn multiply(
a a: Decimal,
b b: Decimal,
) -> Result(Decimal, ArithmeticError)
Multiply two decimals.
pub fn negate(d d: Decimal) -> Decimal
Negate the value. Always safe (the coefficient sign flips but magnitude does not change).
pub fn new(
coefficient coefficient: Int,
exponent exponent: Int,
) -> Decimal
Build a Decimal directly from a coefficient and exponent.
new(coefficient: 1234, exponent: -2) represents 12.34.
Panics on either of the two overflow paths:
|coefficient| > 9_007_199_254_740_991— the coefficient itself exceeds the JS-safe integer ceiling.exponent > 0and|coefficient| × 10^exponent > 9_007_199_254_740_991— the coefficient is safe but the implied rendered value (the integer thatto_stringwould emit) is not.
The panic message distinguishes the two paths so a debugging caller can tell whether to shrink the coefficient or shrink the exponent.
Use try_new when the inputs are supplied by a
caller and might exceed the safe range — that variant returns
a Result instead of panicking.
pub fn rescale(
d d: Decimal,
target_exponent target_exponent: Int,
mode mode: rounding.Mode,
) -> Result(Decimal, ArithmeticError)
Force the decimal to a specific exponent. When the new exponent is
finer (smaller), the coefficient grows by zero-padding (may overflow).
When the new exponent is coarser (larger), digits are dropped using
mode.
pub fn round(
d d: Decimal,
digits digits: Int,
mode mode: rounding.Mode,
) -> Decimal
Round to digits decimal places, trim only. When the input
is already at equal or coarser precision than -digits (e.g.
Decimal(coefficient: 2000, exponent: 0) against digits: 2),
the original Decimal is returned unchanged — round never
pads with zeros, so to_string(round(from_int(2000), 2, _)) is
"2000", not "2000.00".
Use rescale when the result must always have
exponent -digits (i.e. the rendered form must always have
exactly digits decimal places, including trailing zeros) —
rescale returns Result because the padding direction can
overflow ±9_007_199_254_740_991.
pub fn subtract(
a a: Decimal,
b b: Decimal,
) -> Result(Decimal, ArithmeticError)
Subtract b from a.
pub fn to_int(d d: Decimal) -> Result(Int, ArithmeticError)
Convert to a plain integer.
Succeeds only when d is exactly integer-valued — no fractional
part and the resulting integer fits within
±max_safe_coefficient. Both a non-zero fractional remainder and a
coefficient overflow produce PrecisionExceeded. Use
to_int_truncated when the fractional part
should be dropped toward zero, or
to_int_rounded when it should be rounded using
a specific rounding.Mode.
to_int(from_int(7)) // Ok(7)
let assert Ok(d) = from_string("12.34")
to_int(d) // Error(PrecisionExceeded)
let assert Ok(d) = from_string("12.00")
to_int(d) // Ok(12)
pub fn to_int_rounded(
d d: Decimal,
mode mode: rounding.Mode,
) -> Result(Int, ArithmeticError)
Round d to an integer using mode and return that integer.
mode is applied as if rounding to zero decimal places — so
to_int_rounded(d, mode: rounding.HalfEven) is the natural fit
for the “I rounded to N decimals, now give me the integer”
workflow. Returns PrecisionExceeded only when the rounded
integer cannot fit within ±max_safe_coefficient.
pub fn to_int_truncated(
d d: Decimal,
) -> Result(Int, ArithmeticError)
Truncate d toward zero (rounding.Down) and return the integer.
Drops the fractional part regardless of its size, so -1.9 becomes
-1 and 1.9 becomes 1. Use
to_int_rounded when a different rounding mode
is needed. Returns PrecisionExceeded only when the truncated
integer cannot fit within ±max_safe_coefficient (a fractional
part on its own never causes a failure here, unlike
to_int).
pub fn to_string(d d: Decimal) -> String
Render a Decimal as a plain string. Preserves the encoded
exponent (new(coefficient: 100, exponent: -2) renders as "1.00",
not "1").
pub fn truncate(d d: Decimal, digits digits: Int) -> Decimal
Truncate to digits decimal places (rounding toward zero), trim
only. Like round, the input is returned unchanged
when it is already at equal or coarser precision than -digits.
pub fn try_from_int(n n: Int) -> Result(Decimal, ConstructError)
Build a Decimal from an integer, returning a Result.
Returns Error(CoefficientTooLarge) when
|n| > 9_007_199_254_740_991, which is the threshold above
which the resulting Decimal cannot round-trip through
to_string and from_string.
pub fn try_new(
coefficient coefficient: Int,
exponent exponent: Int,
) -> Result(Decimal, ConstructError)
Build a Decimal from a coefficient and exponent, returning a
Result.
Returns Error(CoefficientTooLarge) when the rendered value
would overflow the safe range — either because
|coefficient| > 9_007_199_254_740_991, or because a positive
exponent would push the rendered integer
(|coefficient| × 10^exponent) past that bound. Such a value
cannot round-trip through to_string and
from_string.