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