Apa

Elixir CI status codecov hex.pm version

APA : Arbitrary Precision Arithmetic - pure Elixir implementation.

For arbitrary precision mathematics - which supports numbers of any size and precision up to nearly unlimited of decimals (internal Elixir integer math), represented as strings. Inspired by BCMath/PHP. This is especially useful when working with floating-point numbers, as these introduce small but in some case significant rounding errors.

Intention, Pro & Cons

I started this project to learn for myself - so the focus was on learning and have fun! You could use it if you like - there are some test coverage - but for production I would recomend the Decimal package!

Some limits and 'bugs' in standard Erlang/Elixir:

iex> 0.30000000000000004 - 0.30000000000000003
0.0

with Apa:

"0.30000000000000004" - "0.30000000000000003"
"0.00000000000000001"

Elixir:

iex> 0.1 + 0.2
0.30000000000000004

with Apa:

"0.1" + "0.2"
"0.3"

Elixir:

iex> 9007199254740992.0 - 9007199254740991.0
1.0
iex> 9007199254740993.0 - 9007199254740992.0
0.0
iex> 9007199254740994.0 - 9007199254740993.0
2.0

iex> 87654321098765432.0 - 87654321098765431.0
16.0

iex> 0.123456789e-100 * 0.123456789e-100
1.524157875019052e-202
iex> 0.123456789e-200 * 0.123456789e-200
0.0

iex> :math.pow(2, 1500)
** (ArithmeticError) bad argument in arithmetic expression

On a short research I found the existing lib EAPA have some limits and disadvantages:

EAPA (Erlang/Elixir Arbitrary-Precision Arithmetic) a) Customized precision up to 126 decimal places (current realization) Why only 126 decimal places? Apa should not have that limit!

b) EAPA is a NIF extension written on Rust -> performance fine, but bad in case of dependencies f.e. for Nerves. Apa is in pure Elixir with no dependency - running on any Nerves device.

Later I found Decimal which looks very nice and useful (written by Eric Meadows-Jönsson!) - so there is already a solution - nice, stable and full featured! I used it in Phoenix with Ecto without thinking about it ... but that's life.

Anyway I had fun with Apa on Eastern 2020. ;-)

A little feature I could offer compared to Decimal (but of course could be easily expanded there too)

"0.30000000000000004" - "0.30000000000000003"
"0.00000000000000001"

Or calc and compare directly with strings in case of ecto/database

with Decimal:

schema "products" do
  field :name, :string
  field :price, :decimal
  timestamps()
end

%Product{
  name: "Apple",
  price: 3,
}
cart_total = Decimal.to_string(Decimal.mult(Decimal.new(product.price), Decimal.new(cart_quantity)))

with Apa:

schema "product" do
  field :name, :string
  field :price, :string
  timestamps()
end

%Product{
  name: "Apple",
  price: "3",
}
cart_total = product.price * cart_quantity

Could be useful with CubDB (pure Elixir key/value database).

Features

A list of supported and planned features (maybe incomplete)

  • [x] basic operations (add)
  • [x] basic operations (sub)
  • [x] basic operations (mul)
  • [x] basic operations (div)
  • [x] comparison (comp)
  • [ ] scale (number of digits after the decimal place in the result)
  • [ ] rounding
  • [ ] Infinity and NaN
  • [ ] string format for result
  • [ ] performance - f.e. benchee check - this pure Elixir implementation looks like fast enough for normal applications (normal means not for number crunching)

Installation

  1. Add apa to your list of dependencies in mix.exs:
  def deps do
    [
      {:apa, "~> 0.3.0"}
    ]
  end

Usage

  defmodule ApaExample do
    import Apa
    import Kernel, except: [+: 2, -: 2, *: 2, /: 2, to_string: 1]

    def the_answer() do
      apa1 = Apa.add("1", "2")
      apa2 = Apa.sub("3", "2")

      price = "3.50 Euro"
      quantity = "12"
      total_string = price * quantity

      IO.puts("The Answer to the Ultimate Question of Life, the Universe, and Everything is: ")

      "1"
      |> Apa.add("2")
      |> Apa.add("3")
      |> Apa.sub("4")
      |> Apa.add("5")
      |> Apa.mul("6")
    end
  end

Examples

iex> Apa.add("0.1", "0.2")
"0.3"
iex> Apa.sub("3.0", "0.000000000000000000000000000000000000000000000001")
"2.999999999999999999999999999999999999999999999999"
iex> "333.33" |> Apa.add("666.66") |> Apa.sub("111.11")
"888.88"

iex> "1" |> Apa.add("2") |> Apa.add("3") |> Apa.sub("4") |> Apa.add("5") |> Apa.mul("6")
"42"

:laughing: