FastDecimal (FastDecimal v1.0.0)

Copy Markdown View Source

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

t()

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

coef()

@type coef() :: integer() | :nan | :inf | :neg_inf

rounding_mode()

@type rounding_mode() ::
  :half_even | :half_up | :half_down | :down | :up | :floor | :ceiling

t()

@type t() :: %FastDecimal{coef: coef(), exp: integer()}

to_string_format()

@type to_string_format() :: :normal | :scientific | :raw | :xsd

Output format for to_string/2. :normal is the default.

Functions

abs(fast_decimal)

@spec abs(t()) :: t()

add(a, b)

@spec add(t(), t()) :: t()

cast(d)

@spec cast(t() | integer() | binary() | Decimal.t() | float() | nil) ::
  {:ok, t()} | :error

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.

compare(a, b)

@spec compare(t(), t()) :: :lt | :eq | :gt | :nan

div(a, b, opts \\ [])

@spec div(t(), t(), keyword()) :: t()

Division with configurable precision and rounding.

Options:

  • :precision — number of significant digits to keep in the result (default 28)
  • :rounding:half_even (default, banker's), :half_up, :half_down, :down, :up, :floor, :ceiling

div_int(a, b)

@spec div_int(t(), t()) :: t()

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}

div_rem(arg1, arg2)

@spec div_rem(t(), t()) :: {t(), t()}

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}}

equal?(a, b)

@spec equal?(t(), t()) :: boolean()

Returns true if both decimals compare as equal. NaN never compares equal to anything (matches IEEE 754 behavior for floating-point NaN).

finite?(fast_decimal)

@spec finite?(t()) :: boolean()

Returns true if the value is a finite number (not NaN, not infinity).

from_integer(int)

@spec from_integer(integer()) :: t()

gt?(a, b)

@spec gt?(t(), t()) :: boolean()

inf()

@spec inf() :: t()

Returns the +∞ sentinel value.

inf?(fast_decimal)

@spec inf?(t()) :: boolean()

Returns true if the value is +∞ or -∞.

is_decimal(value)

(macro)

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
end

Mirrors Decimal.Macros.is_decimal/1 so it can be drop-in-substituted.

lt?(a, b)

@spec lt?(t(), t()) :: boolean()

max(a, b)

@spec max(t(), t()) :: t()

min(a, b)

@spec min(t(), t()) :: t()

mult(a, b)

@spec mult(t(), t()) :: t()

multiply(a, b)

See FastDecimal.mult/2.

nan()

@spec nan() :: t()

Returns the NaN sentinel value.

nan?(fast_decimal)

@spec nan?(t()) :: boolean()

Returns true if the value is NaN (not a number).

neg_inf()

@spec neg_inf() :: t()

Returns the -∞ sentinel value.

negate(fast_decimal)

@spec negate(t()) :: t()

negative?(fast_decimal)

@spec negative?(t()) :: boolean()

new(int)

@spec new(String.t() | integer()) :: t()

new(coef, exp)

@spec new(integer(), integer()) :: t()

normalize(d)

@spec normalize(t()) :: t()

Strip trailing zeros from the coefficient, raising the exponent. ~d"1.10" (coef=110, exp=-2) becomes ~d"1.1" (coef=11, exp=-1).

parse(str)

@spec parse(String.t()) :: {:ok, t()} | :error

positive?(fast_decimal)

@spec positive?(t()) :: boolean()

product(list)

@spec product([t()]) :: t()

Product of a list of FastDecimals.

rem(a, b)

@spec rem(t(), t()) :: t()

Remainder of decimal division (same sign as the dividend).

round(decimal, places \\ 0, mode \\ :half_even)

@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}

sigil_d(arg, modifiers)

(macro)

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

sqrt(decimal, opts \\ [])

@spec sqrt(
  t(),
  keyword()
) :: t()

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}

sub(a, b)

@spec sub(t(), t()) :: t()

sum(list)

@spec sum([t()]) :: t()

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.

to_float(fast_decimal)

@spec to_float(t()) :: float()

to_integer(fast_decimal)

@spec to_integer(t()) :: integer()

to_string(decimal, format \\ :normal)

@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, signed E exponent). Matches Decimal's :scientific format.
  • :raw"1234E-5" (raw coefficient + E + raw exponent). Useful for debugging the internal representation.
  • :xsd — XML Schema canonical decimal form. Same as :normal for our representation since we don't use scientific in XSD.

Special values (NaN, Infinity, -Infinity) print the same in every format.

zero?(fast_decimal)

@spec zero?(t()) :: boolean()