elixirmoney / money

Elixir library for working with Money safer, easier, and fun... Is an interpretation of the Fowler's Money pattern in fun.prog.
https://hex.pm/packages/money/
MIT License
826 stars 139 forks source link

Parsing with Float #175

Closed teamon closed 1 year ago

teamon commented 2 years ago

While migrating an app from integer to bigint db types I've noticed that Money does not handle big values properly:

iex(2)> Money.parse "$1.01"
{:ok, %Money{amount: 101, currency: :USD}}

iex(3)> Money.parse "$1,000.01"
{:ok, %Money{amount: 100001, currency: :USD}}

iex(4)> Money.parse "$1,000,000.01"
{:ok, %Money{amount: 100000001, currency: :USD}}

iex(5)> Money.parse "$1,000,000,000.01"
{:ok, %Money{amount: 100000000001, currency: :USD}}

iex(6)> Money.parse "$1,000,000,000,000.01"
{:ok, %Money{amount: 100000000000001, currency: :USD}}

iex(7)> Money.parse "$1,000,000,000,000,000.01"
{:ok, %Money{amount: 100000000000000000, currency: :USD}}             <= problem (1¢ lost)

iex(8)> Money.parse "$1,000,000,000,000,000,000.01"
{:ok, %Money{amount: 100000000000000000000, currency: :USD}}          <= problem (1¢ lost)

iex(9)> Money.parse "$1,000,000,000,000,000,000,000.01"
{:ok, %Money{amount: 99999999999999991611392, currency: :USD}}        <= problem

iex(10)> Money.parse "$1,000,000,000,000,000,000,000,000.01"
{:ok, %Money{amount: 100000000000000004764729344, currency: :USD}}    <= problem

The problem seems to be coming from the usage of Float.parse/1

iex(10)> {f,_}=Float.parse("1000.01"); round(f * 100)                    
100001
iex(11)> {f,_}=Float.parse("1000000.01"); round(f * 100)
100000001
iex(12)> {f,_}=Float.parse("1000000000.01"); round(f * 100)
100000000001
iex(13)> {f,_}=Float.parse("1000000000000.01"); round(f * 100)
100000000000001
iex(14)> {f,_}=Float.parse("1000000000000000.01"); round(f * 100)
100000000000000000
iex(15)> {f,_}=Float.parse("1000000000000000000.01"); round(f * 100)
100000000000000000000
iex(16)> {f,_}=Float.parse("1000000000000000000000.01"); round(f * 100)
99999999999999991611392
iex(17)> {f,_}=Float.parse("1000000000000000000000000.01"); round(f * 100)
100000000000000004764729344

Would it make sense to parse with Decimal.parse? Optionally if the decimal dependency is not available fallback to manual parsing of left & right sides of decimal point and combining using only integers.

tensor-programming commented 2 years ago

I've been running into similar problems when trying to use custom currencies with decimal values larger than 15. With Ethereum for example, the amount of precision seems to drastically drop when compared to a currency that requires fewer decimals. If I had to guess, like you mentioned it comes back to the Float.parse call because precision is really problematic when dealing with float to money conversions especially.

joselo commented 2 years ago

I have a similar issue, basically I need to get the percentage of an amount but the percentage is decimal, i.e

iex> 100 * 0.125
12.5

iex> Money.new(100, :USD) |> Money.multiply(0.125)
%Money{amount: 13, currency: :USD}

I'm not sure if it should return %Money{amount: 1250, currency: :USD} instead of %Money{amount: 13, currency: :USD}

Nitrino commented 1 year ago

To have higher precision, you need to add Decimal to the project dependencies. After that the library will use Decimal instead of Float