Apa
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
- Add
apa
to your list of dependencies inmix.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: