money: Unexpected result when doing arithmetic on Money objects
204.87 + (204.87/100.0) * 22.4 results in 250.76088000000001
Money.new(20487).to_f + (Money.new(20487).to_f/100.0) * 22.4 results in 250.76088000000001
Money.new(20487) + (Money.new(20487)/100.0) * 22.4 results in #<Money fractional:25079 currency:GBP>
I would expect it to be #<Money fractional:25076 currency:GBP> is this a bug?
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Comments: 18 (13 by maintainers)
Commits related to this issue
- Add USD with SubCents as currecny called us8 taken from https://github.com/RubyMoney/money/issues/667#issuecomment-290967583 — committed to lpichler/manageiq-consumption by lpichler 7 years ago
- Add USD with SubCents as currecny called us8 taken from https://github.com/RubyMoney/money/issues/667#issuecomment-290967583 — committed to lpichler/manageiq-consumption by lpichler 7 years ago
@kernow - This is a classic rounding loss scenario. Anytime you multiple or divide Money objects, ESPECIALLY with odd dollar or cent values or decimal rate factors (APRs, Yields, etc…) you can encounter loss of precision. To avoid this, you have to add room on your Money objects for the fractional cents (i.e. subcents). I’ve done this in the past by defining a custom currency with room for 8 subcents as opposed to the usual 2.
The pegged 1:1 exchange rate is key. This is how you round off the fractional cents at the end to get to the value you’re looking for.
Results in a precise value
#<Money fractional:25076088000 currency:US8>In order to get the rounded value, exchange back to your base currency.
=>
#<Money fractional:25076 currency:USD>In short, this isn’t a bug.
moneymakes no assumptions about how to round. If you need this level of precision, I recommend defining a custom currency to carry the fractional cents.@antstorm - I think I’ve answered this completely and you can close this issue?
@antstorm I think I have to 🙂
I’m thinking about all this stuff right now, and it looks like proper guidance is the most important thing here, because whatever we implement, it won’t solve most problems anyway.
Because we calculate the money in order to store them somewhere most likely. And we are supposed to store rounded money, the ones that are finalized and do make sense, right?
So, the only actually “dangerous” operations are division and multiplication (as we can multiply by a fractional number). One must avoid them as much as possible.
For example, let’s say we have to split some uneven amount like $9.99 into two separate ones. Let’s say it should be split in a ratio of 75% and 25%. We can do it like so:
So far so good with default rounding mode (749.25 is rounded down, and 249.75 is rounded up). ! Your rounding mode could be different though ! Also 50/50% split is a corner case:
Whoops! We got an error here! So what could we do in order to avoid it? Avoid multiplication. So we should have this instead:
That’s better.
One might think an error is too small to worry about. Well, maybe in some cases it’s OK. But what if you have validations? Like “
a + bmust be equal topayment” for example? The code might fail it’s own validations.So one must be sure the validations are being done in the same way the calculations are. Or maybe add some delta to them? Or maybe just get rid of them? 😉
@danielricecodes, I guess I didn’t mean to avoid them all the time, but in some cases, like splitting 😃 And it turns out we got that sweet
Money#allocate, thanks @antstorm! We definitely have to advise people on using it!@FunkyloverOne that makes sense, however it’s not always trivial, imagine if you need to split money 3-way, or 100-way? Have you looked at
Money#allocate? I think we should advice on using it when dealing with the use-case you’ve described.But yeah, that problem definitely exists as we’re resetting the error every step of the way.
infinite_precisionmight be another option here