Fast arbitrary-precision decimal arithmetic for Elixir.
A decimal is represented as coef * 10^exp where coef is a BEAM integer
(sign carried inline) and exp is an integer exponent. Operations work on
raw integers in the hot path; values that exceed 60-bit immediate ints
promote to BEAM bignums automatically.
Design: exact arithmetic, explicit precision
Unlike Decimal, FastDecimal does not maintain an implicit per-process
precision context. add, sub, and mult are mathematically exact — the
result coefficient grows to whatever size is needed. Only div/3 takes a
precision argument, because division is the only operation that can produce
a non-terminating decimal.
If you want bounded precision after an arithmetic chain, call round/2 or
normalize/1 explicitly. Trading implicit context for explicit calls is
faster (no process-dict lookup per op) and easier to reason about.
Construction
iex> FastDecimal.new("1.23")
%FastDecimal{coef: 123, exp: -2}
iex> FastDecimal.new(123, -2)
%FastDecimal{coef: 123, exp: -2}Use the ~d sigil for compile-time literals (zero runtime parse cost):
import FastDecimal
~d"1.23" # => %FastDecimal{coef: 123, exp: -2}Arithmetic
iex> FastDecimal.add(FastDecimal.new("1.23"), FastDecimal.new("4.567"))
%FastDecimal{coef: 5797, exp: -3}
iex> FastDecimal.div(FastDecimal.new("10"), FastDecimal.new("3"), precision: 5)
%FastDecimal{coef: 33333, exp: -4}Comparison
iex> FastDecimal.compare(FastDecimal.new("1.10"), FastDecimal.new("1.1"))
:eq
iex> FastDecimal.equal?(FastDecimal.new("1.10"), FastDecimal.new("1.1"))
true
Summary
Types
Output format for to_string/2. :normal is the default.
Functions
Soft parse: returns {:ok, t()} or :error without raising. Accepts the
same inputs as new/1 plus existing FastDecimal and Decimal structs.
Division with configurable precision and rounding.
Integer (truncated) division. Like Kernel.div/2 for integers — drops the
fractional part, truncating toward zero. Result always has exp: 0.
Returns {quotient, remainder} such that a == quotient * b + remainder.
Quotient is computed by div_int/2.
Returns true if both decimals compare as equal. NaN never compares equal to anything (matches IEEE 754 behavior for floating-point NaN).
Returns true if the value is a finite number (not NaN, not infinity).
Returns the +∞ sentinel value.
Returns true if the value is +∞ or -∞.
Guard-safe predicate. True when the argument is a %FastDecimal{} struct.
Returns the NaN sentinel value.
Returns true if the value is NaN (not a number).
Returns the -∞ sentinel value.
Strip trailing zeros from the coefficient, raising the exponent.
~d"1.10" (coef=110, exp=-2) becomes ~d"1.1" (coef=11, exp=-1).
Product of a list of FastDecimals.
Remainder of decimal division (same sign as the dividend).
Round to places decimal places using the given rounding mode.
Compile-time literal sigil. ~d"1.23" becomes a %FastDecimal{} at compile
time, paying zero parse cost at runtime.
Square root with configurable precision (default 28 significant digits).
Sum a list of FastDecimals. Equivalent to Enum.reduce(list, new(0), &add/2)
but inlined and recursion-flat. The tight inner loop avoids Enum's anonymous
function call overhead.
Format a decimal as a string. format defaults to :normal.
Types
@type coef() :: integer() | :nan | :inf | :neg_inf
@type rounding_mode() ::
:half_even | :half_up | :half_down | :down | :up | :floor | :ceiling
@type to_string_format() :: :normal | :scientific | :raw | :xsd
Output format for to_string/2. :normal is the default.
Functions
Soft parse: returns {:ok, t()} or :error without raising. Accepts the
same inputs as new/1 plus existing FastDecimal and Decimal structs.
This is what Ecto's Ecto.Type machinery calls — exposing it directly
makes user code that needs "try to coerce, otherwise complain" pleasant.
Division with configurable precision and rounding.
Options:
:precision— number of significant digits to keep in the result (default28):rounding—:half_even(default, banker's),:half_up,:half_down,:down,:up,:floor,:ceiling
Integer (truncated) division. Like Kernel.div/2 for integers — drops the
fractional part, truncating toward zero. Result always has exp: 0.
iex> FastDecimal.div_int(FastDecimal.new("10.5"), FastDecimal.new("3"))
%FastDecimal{coef: 3, exp: 0}
Returns {quotient, remainder} such that a == quotient * b + remainder.
Quotient is computed by div_int/2.
iex> FastDecimal.div_rem(FastDecimal.new("10"), FastDecimal.new("3"))
{%FastDecimal{coef: 3, exp: 0}, %FastDecimal{coef: 1, exp: 0}}
Returns true if both decimals compare as equal. NaN never compares equal to anything (matches IEEE 754 behavior for floating-point NaN).
Returns true if the value is a finite number (not NaN, not infinity).
@spec inf() :: t()
Returns the +∞ sentinel value.
Returns true if the value is +∞ or -∞.
Guard-safe predicate. True when the argument is a %FastDecimal{} struct.
defmodule MyMod do
require FastDecimal
def total(d) when FastDecimal.is_decimal(d) do
# ...
end
endMirrors Decimal.Macros.is_decimal/1 so it can be drop-in-substituted.
See FastDecimal.mult/2.
@spec nan() :: t()
Returns the NaN sentinel value.
Returns true if the value is NaN (not a number).
@spec neg_inf() :: t()
Returns the -∞ sentinel value.
Strip trailing zeros from the coefficient, raising the exponent.
~d"1.10" (coef=110, exp=-2) becomes ~d"1.1" (coef=11, exp=-1).
Product of a list of FastDecimals.
Remainder of decimal division (same sign as the dividend).
@spec round(t(), integer(), rounding_mode()) :: t()
Round to places decimal places using the given rounding mode.
Default: 0 places, :half_even (banker's rounding).
Supported modes: :half_even, :half_up, :half_down, :down, :up,
:floor, :ceiling.
iex> FastDecimal.round(FastDecimal.new("1.235"), 2)
%FastDecimal{coef: 124, exp: -2}
iex> FastDecimal.round(FastDecimal.new("1.236"), 2, :down)
%FastDecimal{coef: 123, exp: -2}
iex> FastDecimal.round(FastDecimal.new("123.456"), -1)
%FastDecimal{coef: 12, exp: 1}
Compile-time literal sigil. ~d"1.23" becomes a %FastDecimal{} at compile
time, paying zero parse cost at runtime.
Use by importing: import FastDecimal, then ~d"1.23".
Square root with configurable precision (default 28 significant digits).
Newton-Raphson on bigints; converges in ~log(digits) iterations because the initial guess uses the number's digit count.
iex> FastDecimal.sqrt(FastDecimal.new("4"))
%FastDecimal{coef: 2, exp: 0}
iex> FastDecimal.sqrt(FastDecimal.new("2"), precision: 10)
%FastDecimal{coef: 1414213562, exp: -9}
Sum a list of FastDecimals. Equivalent to Enum.reduce(list, new(0), &add/2)
but inlined and recursion-flat. The tight inner loop avoids Enum's anonymous
function call overhead.
@spec to_string(t(), to_string_format()) :: String.t()
Format a decimal as a string. format defaults to :normal.
:normal—"1234.5678","0.001","123"(decimal-point form when there are fractional digits, plain integer otherwise):scientific—"1.2345678E+3"(one digit before decimal, signedEexponent). Matches Decimal's:scientificformat.:raw—"1234E-5"(raw coefficient +E+ raw exponent). Useful for debugging the internal representation.:xsd— XML Schema canonical decimal form. Same as:normalfor our representation since we don't use scientific in XSD.
Special values (NaN, Infinity, -Infinity) print the same in every format.