Packages

A drop-in :decimal module reproducing the legacy erlang_decimal API on top of the modern Elixir Decimal library, so legacy code and the Elixir decimal app can coexist in one BEAM node.

Current section

Files

Jump to
fake_decimal lib fake_decimal.ex
Raw

lib/fake_decimal.ex

defmodule :decimal do
@moduledoc """
This library provides a `:decimal` module that has the same functions (API,
interface) as the good^H^H^H^Hbad old `erlang_decimal` library, but it is
just an interface to the Elixir `Decimal` library.
The old `erlang_decimal` used 2-tuples with integers in it to represent
numbers, while the Elixir `decimal` uses structs. By using this library, the
module `:decimal` serves as the old API, and the module `Decimal` as the new
one. Unfortunately, the application names clash in the package `decimal` and
`erlang_decimal`, so you cannot use them at once, but `fake_decimal` plays
nicely with the Elixir `decimal`.
If there's a legacy application that calls into `:decimal`, but you also need
the Elixir `decimal` library in your app, you may be able to provide the old
API for the legacy app by using `fake_decimal`.
The functions where rounding semantics matter (`round/3`, `divide/3`,
`sqrt/2`, `cmp/3`, `fast_cmp/2`, `to_binary/1,2`) reproduce the original
`erlang_decimal` algorithms exactly: `precision` counts fractional digits
(decimal places, not significant digits), and the `round_half_up` /
`round_half_down` modes decide based on the first discarded digit only,
just like the original.
## Deviations from `erlang_decimal`
* `add/2`, `sub/2` and `mult/2` results are normalized: `add({1, 0}, {9, 0})`
returns `{1, 1}` where the original returns `{10, 0}`. The two tuples are
numerically equal (`cmp/3` returns `0` and `reduce/1` maps both to the
same tuple), but code that pattern-matches exact tuples may see a
different — equivalent — representation.
* Wherever the original accepts only `{coef, exp}` tuples, this module also
accepts Elixir `Decimal` structs, integers, floats, strings and charlists.
"""
import Kernel, except: [abs: 1]
defguardp is_erlang_decimal(d)
when tuple_size(d) == 2 and is_integer(elem(d, 0)) and is_integer(elem(d, 1))
# The pre-erlang_decimal "old" format: {sign, coef, exp} with sign 1 = negative.
defguardp is_old_decimal(d)
when tuple_size(d) == 3 and elem(d, 0) in [0, 1] and is_integer(elem(d, 1)) and
is_integer(elem(d, 2))
############################
#### Converters
def to_decimal(value) when is_erlang_decimal(value), do: value
def to_decimal({0, coef, exp} = value) when is_old_decimal(value), do: {coef, exp}
def to_decimal({1, coef, exp} = value) when is_old_decimal(value), do: {-coef, exp}
def to_decimal(%Decimal{sign: s, coef: c, exp: e}), do: {s * c, e}
def to_decimal(value) when is_integer(value), do: {value, 0}
def to_decimal(value) when is_float(value), do: to_decimal(Decimal.from_float(value))
def to_decimal(value) when is_list(value), do: to_decimal(Decimal.new(to_string(value)))
def to_decimal(value), do: to_decimal(Decimal.new(value))
def to_decimal(value, _opts) when is_erlang_decimal(value) or is_old_decimal(value),
do: to_decimal(value)
def to_decimal(value, %{precision: precision, rounding: rounding}),
do: do_round(rounding, to_decimal(value), precision)
def to_decimal(value, _opts), do: to_decimal(value)
def to_decimal(base, exp, _opts), do: {base, exp}
def to_binary(a), do: format(to_decimal(a), true)
def to_binary(a, opts), do: format(to_decimal(a), opts[:pretty] == true)
############################
#### Arith
def add(a, b), do: exact_lift(:add, a, b)
def sub(a, b), do: exact_lift(:sub, a, b)
def mult(a, b), do: exact_lift(:mult, a, b)
def divide(a, b, opts), do: do_divide(to_decimal(a), to_decimal(b), opts)
defp do_divide({_, _}, {0, _}, _opts), do: :erlang.error(:badarith)
defp do_divide({0, _}, {_, _}, _opts), do: {0, 0}
defp do_divide({base_a, exp_a}, {base_b, exp_b}, opts) when base_b < 0,
do: do_divide({-base_a, exp_a}, {-base_b, exp_b}, opts)
defp do_divide({base_a, exp_a}, {base_b, exp_b}, %{precision: precision0, rounding: rounding}) do
precision = max(0, exp_a - exp_b) + precision0 + 1
base_res = div(base_a * pow10(precision), base_b)
do_round(rounding, {base_res, exp_a - exp_b - precision}, precision0)
end
def sqrt(a, opts), do: do_sqrt(to_decimal(a), opts)
defp do_sqrt({coef, _exp}, _opts) when coef < 0, do: :erlang.error(:badarith)
defp do_sqrt({0, _exp}, _opts), do: {0, 0}
defp do_sqrt({coef, exp} = decimal, %{precision: precision0} = opts) do
precision = precision0 + 1
coef_digits = length(Integer.digits(coef))
if rem(exp, 2) == 0 do
shift = precision - div(coef_digits + 1, 2)
do_sqrt(decimal, opts, shift, coef)
else
shift = precision - (div(coef_digits, 2) + 1)
do_sqrt(decimal, opts, shift, coef * 10)
end
end
defp do_sqrt(decimal, opts, shift, coef) do
if shift >= 0 do
do_sqrt(decimal, opts, shift, coef * pow10(2 * shift), true)
else
operand = pow10(-2 * shift)
do_sqrt(decimal, opts, shift, div(coef, operand), rem(coef, operand) == 0)
end
end
defp do_sqrt({_, exp0}, %{precision: precision, rounding: rounding}, shift, coef, exact) do
exp = Integer.floor_div(exp0, 2)
n = isqrt(coef, pow10(precision + 1))
result =
cond do
exact and n * n == coef and shift >= 0 -> {div(n, pow10(shift)), exp}
exact and n * n == coef -> {n * pow10(-shift), exp}
true -> {n, exp - shift}
end
do_round(rounding, result, precision)
end
defp isqrt(m, n) do
q = div(m, n)
if n <= q, do: n, else: isqrt(m, div(n + q, 2))
end
############################
#### Compare
def cmp(a, b, opts), do: do_cmp(to_decimal(a), to_decimal(b), opts)
defp do_cmp({0, _}, {0, _}, _opts), do: 0
defp do_cmp({coef1, _}, {coef2, _}, _opts) when coef1 >= 0 and coef2 <= 0, do: 1
defp do_cmp({coef1, _}, {coef2, _}, _opts) when coef1 <= 0 and coef2 >= 0, do: -1
defp do_cmp({coef, exp}, {coef, exp}, _opts), do: 0
defp do_cmp({coef1, exp}, {coef2, exp}, _opts) when coef1 > coef2, do: 1
defp do_cmp({coef1, exp}, {coef2, exp}, _opts) when coef1 < coef2, do: -1
defp do_cmp(a, b, %{precision: precision, rounding: rounding}),
do: exact_cmp(do_round(rounding, a, precision), do_round(rounding, b, precision))
defp do_cmp(a, b, _opts), do: exact_cmp(a, b)
def fast_cmp(a, b), do: exact_cmp(to_decimal(a), to_decimal(b))
defp exact_cmp({coef1, exp1}, {coef2, exp2}) do
exp_min = min(exp1, exp2)
scaled1 = coef1 * pow10(exp1 - exp_min)
scaled2 = coef2 * pow10(exp2 - exp_min)
cond do
scaled1 < scaled2 -> -1
scaled1 > scaled2 -> 1
true -> 0
end
end
############################
#### Utils
def abs(a) do
{coef, exp} = to_decimal(a)
{Kernel.abs(coef), exp}
end
def minus(a) do
{coef, exp} = to_decimal(a)
{-coef, exp}
end
def is_zero(a), do: match?({0, _}, to_decimal(a))
def reduce(a), do: reduce_coef(to_decimal(a))
def round(mode, num, precision \\ 0), do: do_round(mode, to_decimal(num), precision)
############################
#### Helpers
# The original computes add/sub/mult exactly, so widen the context enough
# to hold the full result before delegating to the Elixir Decimal library.
defp exact_lift(function, a, b) do
{coef1, exp1} = to_decimal(a)
{coef2, exp2} = to_decimal(b)
digits1 = length(Integer.digits(coef1))
digits2 = length(Integer.digits(coef2))
needed =
case function do
:mult -> digits1 + digits2
_ -> max(digits1 + exp1, digits2 + exp2) - min(exp1, exp2) + 1
end
context = Decimal.Context.get()
context = %{context | precision: max(context.precision, needed)}
Decimal.Context.with(context, fn -> lift(Decimal, function, [a, b]) end)
end
defp lift(module, function, args) do
apply(module, function, Enum.map(args, &to_elixir_decimal/1))
|> case do
%Decimal{} = value -> to_decimal(Decimal.apply_context(Decimal.normalize(value)))
other -> other
end
end
defp do_round(rounding, {_coef, exp} = decimal, precision) do
case -precision - exp do
delta when delta > 0 -> reduce_coef(round_coef(rounding, decimal, delta))
_ -> reduce_coef(decimal)
end
end
defp round_coef(:round_down, {coef, exp}, delta) do
zero_exp(div(coef, pow10(delta)), exp + delta)
end
defp round_coef(mode, {coef, exp}, delta) when mode in [:round_ceiling, :round_floor] do
p = pow10(delta)
base = div(coef, p)
diff = coef - base * p
base =
cond do
mode == :round_floor and diff < 0 -> base - 1
mode == :round_ceiling and diff > 0 -> base + 1
true -> base
end
zero_exp(base, exp + delta)
end
defp round_coef(mode, {coef, exp}, delta) do
# Like the original, the half modes only look at the first discarded digit.
data = div(coef, pow10(delta - 1))
base = div(data, 10)
last_digit = Kernel.abs(data - base * 10)
base =
cond do
mode == :round_half_up and last_digit >= 5 and data > 0 -> base + 1
mode == :round_half_up and last_digit >= 5 and data < 0 -> base - 1
mode == :round_half_down and last_digit > 5 and data > 0 -> base + 1
mode == :round_half_down and last_digit > 5 and data < 0 -> base - 1
true -> base
end
zero_exp(base, exp + delta)
end
defp zero_exp(0, _exp), do: {0, 0}
defp zero_exp(coef, exp), do: {coef, exp}
defp reduce_coef({0, _exp}), do: {0, 0}
defp reduce_coef({coef, exp}) do
if rem(coef, 10) == 0, do: reduce_coef({div(coef, 10), exp + 1}), else: {coef, exp}
end
defp format({coef, 0}, _pretty), do: "#{coef}.0"
defp format({coef, exp}, pretty) do
sign = if coef < 0, do: "-", else: ""
digits = Integer.to_string(Kernel.abs(coef))
adjusted = byte_size(digits) + exp - 1
cond do
exp < 0 and (not pretty or adjusted > -6) ->
if adjusted < 0 do
sign <> "0." <> String.duplicate("0", -(adjusted + 1)) <> digits
else
{int_part, frac_part} = String.split_at(digits, adjusted + 1)
sign <> int_part <> "." <> frac_part
end
exp >= 0 and (not pretty or adjusted < 6) ->
sign <> digits <> String.duplicate("0", exp) <> ".0"
byte_size(digits) == 1 ->
sign <> digits <> ".0" <> exp_suffix(adjusted)
true ->
{first, rest} = String.split_at(digits, 1)
sign <> first <> "." <> rest <> exp_suffix(adjusted)
end
end
defp exp_suffix(0), do: ""
defp exp_suffix(exp), do: "e#{exp}"
defp pow10(n) when n > 0, do: Integer.pow(10, n)
defp pow10(_n), do: 1
defp to_elixir_decimal({c, e} = d) when is_erlang_decimal(d),
do: Decimal.new(sign(c), Kernel.abs(c), e)
defp to_elixir_decimal(value) when is_float(value), do: Decimal.from_float(value)
defp to_elixir_decimal(value), do: Decimal.new(value)
defp sign(x) when x < 0, do: -1
defp sign(_), do: 1
end