Packages
fake_decimal
0.1.1
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
Current section
Files
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