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:

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.