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)
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 if the implied rendered value exceeds the safe range (|coefficient| > 9_007_199_254_740_991, or — for non-negative exponent|coefficient| × 10^exponent > 9_007_199_254_740_991). 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, 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.

pub fn zero() -> Decimal

The decimal value 0.

Search Document