I noticed some potential pitfalls with arithmetic operations, both on a technical point and conceptually.
The technical issue applies to all (add/2
, subtract/2
, multiply/2
) when allowing floats as an input type, see: https://github.com/liuggio/money/blob/master/lib/money.ex#L251
While the library is right in assuming you want to avoid using floats to store currency, exposing functionality like this is still just as much of a problem. Assume the floating point value is of the kind that is representable (i.e. the compiler isn't going to map it from one value into the closest representation, but it actually can represent it exactly), the issue arises in the operation itself. Since if you multiply it by 100, it may no longer be exactly representable (may incur some loss of precision).
An easy way to demonstrate this is with the last binary64 value that can still represent a fractional part: 4503599627370495.50
(exponent: 0x432, mantissa: 0xfffffffffffff). Any value greater than that will be a binary64 that can no longer represent a fractional part (its level of precision is now up to whole numbers). This causes very noticeable results when multiplying it by 100: 4503599627370495.50 * 100 = 450359962737049536.00
. Now the subunit of that money is incorrect, it's at 36 cents instead of 50 cents.
value = Money.new(0, :USD)
Money.add(value, 4503599627370495.50) #=> %Money{amount: 450359962737049536, currency: :USD}
Worse still if we're working with values even bigger than this, say values large enough where there is a loss of precision in whole numbers then that operation will now cause the money value to be off by whole dollar amounts (and the higher you keep going the greater the range of error becomes).
The only safe way to handle this is to not do any operations with floating points. Rather extract the whole number portion (so using trunc/1) and then extract the fractional portion (unfortunately I'm not aware of an elixir or erlang function to do this, worst case would be extracting the exponent and mantissa portions as integers using a binary and then calculating it from that). Oh the joys of floating point numbers 😝.
The other potential problem I noticed (unsure if intentional or not) is just a conceptual problem. It looks like the library is assuming all currencies or monetary values will be in subunits of 1/100. The problem here is not all currencies share this common trait, some currencies have no subunits, while others go up to 1/1000, while others don't even use a subunit that's divisible by 10 (instead something like 5). The other problem is this is regarding typical denominations, electronically many currencies aren't restricted to these units. Some financial services will operate on money with many levels of precision (not just what that given currency's denominable subunit may be).