4 ms·
> At the point it's stored again, it should be converted to an exact representation again. Partial cents are not valid amounts of currency to have, so it makes
by smallnamespace 8y ago
> At the point it's stored again, it should be converted to an exact representation again. Partial cents are not valid amounts of currency to have, so it makes sense to deal with the difference before it is represented as a currency again.
Exact decimal representation doesn't prevent rounding errors from accumulating over a chain of computations because oftentimes the correct value cannot be represented as an exact decimal. Forcing it by repeatedly rounding to the nearest currency unit, which is what you're proposing above, compounds the problem by needlessly throwing away available precision.
Simple example is paying interest on a bank account. If you insist on only storing account balances as an exact 2-digit decimal number, then any compound interest computation will quickly accumulate large errors, e.g.:
// $100 in 'exact' integer form
let float_balance = 10000
let rounded_balance = 10000
const daily_interest = 0.10 / 365 // 10% rate, compounded daily
// Pay interest for 5 years
for (let i = 0; i < 365 * 5; i++) {
float_balance *= (1 + daily_interest)
// This number goes into my DB, so I always round to the nearest penny!!!
rounded_balance = Math.round(rounded_balance * (1 + daily_interest))
}
console.log(Math.round(rounded_balance) / 100) // 163.74
console.log(Math.round(float_balance) / 100) // 164.86
console.log('missing interest: $' +
Math.round(float_balance - rounded_balance) / 100)
// missing interest: $1.12
'Exact representation' does not ensure that your computation remains free from roundoff error, and sometimes makes it much worse. If you really insist on 'only storing as many digits as there exist in the currency' in your DB, then you will systematically underpay interest to all your customers... in this example, you'll actually start shortchanging your customer in just a few weeks.
It's your extra forced rounding to pennies that introduces the error: in reality, you really do owe fractional cents before they tick over into the next penny, even if you don't show it in the bank statement. The easiest way to solve the issue here is to store the account balances as floats, which keep track of the 'fractional pennies' which you claim are meaningless. Or forget about floats -- just use fixnums, but keep are the lower digits rather than discarding them upon storage.
By the way, this is the whole point of numerical analysis — you cannot blindly assert that 'if my numerical representation is exact, then my computation will always be correct / minimize total errors'. Yes, the logic is simple and seductive. The conclusion is also completely, utterly, stupendously, insidiously wrong. I hope you don't actually work in a financial context, because this will bite you in the ass someday if you lean hard on it.
I don't really like to appeal to authority, but it was literally my day job for a few years to make sure the numbers were right. Half my team were physics PhDs, and the rest were usually math PhDs or MFEs. We didn't use floats because we were sloppy, we used them because they typically were the most reliable way to get the most accurate result, in a domain where most numbers represented currency or money.
Only when you are actually sending or receiving money from the outside world (e.g. on an invoice, wire transfer, or transaction) should you rounding to the nearest decimal, and at that point, it might be appropriate to use a decimal or fixnum type to exactly represent the monetary value that was actually moved, but again this is typically overkill since you can always just store the rounded amount as a float. This is safe because rounding is idempotent: round(round(float)) = round(float)
But internal monetary calculations are best kept in the same type with enough extra precision to do computations, and floats are often the correct choice here, with no forced intermediate rounding. If you need 2 accurate decimal digits and you insist on using fixnums rather than floats, then you should be passing around at least micros ($1e-6).