FakeDecimal
If you deal with legacy Erlang code, you may know the old erlang_decimal
library. On Hex it's published as erlang_decimal, but it registers its OTP
application (and its module) under the plain name decimal — the same name the
modern Elixir decimal library uses for its own OTP application. Since a BEAM
node can't run two applications with the same name, you can't depend on both at
once.
fake_decimal solves this by providing the exact same API as the old decimal
module, implemented on top of the modern Elixir Decimal library. Depend on
fake_decimal (which pulls in the real decimal app) instead of
erlang_decimal, and your legacy code can keep calling :decimal.add/2,
:decimal.round/3, and so on without ever knowing it's talking to the new
library underneath.
Installation
Add fake_decimal to your list of dependencies in mix.exs:
def deps do
[
{:fake_decimal, "~> 0.1.1"}
]
end
Documentation is available on HexDocs.
The decimal.hrl header
erlang_decimal ships an include/decimal.hrl with convenience macros
(?add/2, ?divide/2, ?to_binary/1, ?d_context, …) that wrap the plain
decimal functions. fake_decimal ships the same header, unchanged — the
macros only expand to decimal:... calls, which is exactly what this library
provides.
The one thing you have to adjust in legacy modules is the include path, because
the OTP application is now called fake_decimal:
%% -include_lib("decimal/include/decimal.hrl").
-include_lib("fake_decimal/include/decimal.hrl").
The knobs the header honours are unchanged too, so you can still override them before including it:
-define(d_precision, 20).
-define(d_rounding, round_down).
-define(d_pretty, false).
-define(d_no_autocast, true). %% skip the implicit ?to_decimal/1 on arguments
-include_lib("fake_decimal/include/decimal.hrl").
Modules and types
Both modules of the original library are provided: decimal and
decimal_conv. The latter — parsing, formatting and float conversion — is a
direct port of the original Erlang source rather than a shim, because it is
what decides which strings are valid and which digits a float produces.
All four exported types are reproduced with identical definitions, so -specs
in legacy modules keep resolving under Dialyzer:
-spec total(decimal:decimal(), decimal:decimal()) -> decimal:decimal().
-spec context() -> decimal:opts().
-spec mode() -> decimal:rounding_algorithm().
-spec print_opts() -> decimal_conv:binary_opts().
Known differences
Two remain, both of which only ever accept more or return an equal value, so neither can break code that worked against the original:
add/2,sub/2andmult/2return normalized tuples —add({1,0}, {9,0})gives{1,1}where the original gives{10,0}. Numerically equal, andreduce/1maps both to the same tuple, but exact pattern matches will see the difference.Functions that take a
{coef, exp}tuple in the original also accept the old{sign, coef, exp}format,Decimalstructs, integers, floats, strings and charlists here.
Everything else — rounding, division, square root, comparison, formatting, parsing, and the errors raised on invalid input — matches the original.
Property-based tests compare fake_decimal against a real instance of
erlang_decimal, loaded in a separate node, to make sure the two behave
identically.
See the @moduledoc in lib/fake_decimal.ex for the full API and the handful of
intentional deviations from the original.