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

  • DivisionByZero

    The right-hand operand of a divide was zero.

  • PrecisionExceeded

    The result would not fit in the supported precision window (±9_007_199_254_740_991). Reduce intermediate precision with round and retry.

Errors returned by validated constructors (try_new, try_from_int).

pub type ConstructError {
  CoefficientTooLarge
}

Constructors

  • CoefficientTooLarge

    The supplied coefficient — or the value implied once the exponent is applied (|coefficient| × 10^exponent for exponent ≥ 0) — would exceed ±9_007_199_254_740_991 (the JavaScript-safe integer ceiling). Such a value cannot round-trip through to_string and from_string, so construction fails fast rather than producing a Decimal the 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 field is either "thousands" or "decimal"; value is the offending input echoed back so the caller can route the failure to its locale layer.

  • SeparatorsCollide(value: String)

    The thousands and decimal_separator arguments 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.

  • EmptyDecimalSeparator

    The decimal_separator argument 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

  • EmptyInput

    The 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.

  • MultipleDecimalPoints

    The input contained more than one decimal point.

  • MultipleSigns

    The input contained more than one sign character.

  • NoDigits

    The input contained no digits (e.g. "+", ".", "-.").

  • ParsedValueTooLarge

    The 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 from to_string), parsing fails fast.

Values

pub fn absolute(d d: Decimal) -> Decimal

Absolute value. Always safe.

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 coefficient(d d: Decimal) -> Int

The signed coefficient component.

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 (Python decimalwithprec=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 exponent(d d: Decimal) -> Int

The exponent component (base 10).

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:

  • thousands or decimal_separator with more than one grapheme (the function is a single-character separator formatter; longer inputs almost always indicate a config-pipeline bug)
  • identical thousands and decimal_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.20.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 FloatString 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)

Whitespace. Leading and trailing Unicode whitespace is trimmed before parsing — not only ASCII space / tab / newline but also NO-BREAK SPACE (U+00A0), NARROW NO-BREAK SPACE (U+202F), IDEOGRAPHIC SPACE (U+3000), the en/em-space family (U+2000–U+200A), and the line / paragraph separators (U+2028 / U+2029). This matches the Unicode White_Space property, so values copied from web pages, locale currency formatting, or CSV / Excel exports parse without the caller pre-cleaning them. Whitespace between digits is still rejected (e.g. "1 . 5" and "1\u{00a0}5" both fail) — trimming is at the ends only.

pub fn is_negative(d d: Decimal) -> Bool

Test for a strictly negative decimal.

pub fn is_positive(d d: Decimal) -> Bool

Test for a strictly positive decimal.

pub fn is_zero(d d: Decimal) -> Bool

Test for the zero decimal.

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 > 0 and |coefficient| × 10^exponent > 9_007_199_254_740_991 — the coefficient is safe but the implied rendered value (the integer that to_string would 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 one() -> Decimal

The decimal value 1.

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. The result always carries exactly digits fractional places: a coarser-precision input is zero-padded (so to_string(round(from_int(2000), 2, _)) is "2000.00"), a finer-precision input is rounded using mode, and digits == 0 yields a plain integer rendering. This is the only useful contract for monetary formatting — to_string(round(d, 2, _)) is "12.30", never "12.3".

round is the non-failing form of rescale targeting exponent -digits; the two share one implementation so their results cannot drift. Padding can in principle overflow ±9_007_199_254_740_991 (only reachable on the JavaScript target); when it would, round falls back to returning the input unchanged rather than failing. Reach for rescale directly when you need to observe that overflow as an Error.

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). Like round, the result carries exactly digits fractional places — a coarser-precision input is zero-padded — but the rounding direction is always toward zero (rounding.Down).

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.

pub fn zero() -> Decimal

The decimal value 0.

Search Document