A while back I did some exhaustive testing of several ways to calculate sales tax on a sale in Python3 and JavaScript, testing the various "obvious" ways against all tax rates in increments of 0.0001 from 0.0000 to 1.0000 and all sale amount from 0.00 to something like 25.00.
It was interesting. Here are some simple cases to try. One or more of these break most of the seemingly obvious approaches:
1% of $21.50
3% of $21.50
6% of $21.50
10% of $21.15
Here are some examples of obvious looking but wrong approaches. These all return the tax in pennies, so the desired results are 22, 65, 129, and 212. In puts are floats, such as 0.01 for 1% and 21.50 for $21.50.
# 1%, 10% wrong
def tax_f1(amt, rate):
tax = round(amt * rate,2)
return round(tax * 100)
# 3%, 10% wrong
def tax_f2(amt, rate):
return round(amt*rate*100)
# 6% wrong
def tax_f3(amt, rate):
return round(amt*rate*100+.5)
Here are two that do work:
def tax_f4(amt, rate):
amt = round(amt * 100)
rate = round(rate * 10000)
tax = (amt * rate + 5000)//10000
return tax
def tax_f5(amt, rate):
amt = int(amt * 100 + .5)
rate = int(rate * 10000 + .5)
tax = (amt * rate + 5000)//10000
return tax
If instead of floating point input, you work with numbers already scaled up to integers (scaling the rates by 10^4 and the money by 10^2), this works:
def tax(amt, rate):
tax = (amt * rate + 5000)//10000
return tax
The last three approaches, (tax, tax_f5, and tax_f4) also work well in PHP, Perl, and JavaScript.
In my code, I attach a suffix to rates and moneys that are scaled this way telling the scale factor, so I might have tax4 and cost2, meaning the tax rate is x10^4 and the cost is x10^2. I was disappointed to find that neither Perl nor Python would let my use Unicode subscript numbers in my variable names. :-(
I also did a series of exhaustive tests where I'd take every string of the form every string of the form digits dot digits with up to N digits after the dot, parse them with the languages most natural string to float converter, multiple that by 10^N and round to an integer, and verify that this was the "right" integer, and also verified that doing a floating point divide of that integer by 10^N and then using the most natural way in the language to turn that to a string with N digits after the dot gave the right result.
The idea here was to convince myself that I would not run into problems at the "convert to integer" or the "convert from integer" part. I did these tests in C, Perl, Python, and JavaScript I think. I'm having trouble finding my code now so I'm not sure if I tested in all of them.
Anyway, I concluded that it was safe. It's only when you start computing in float point that you have to worry.
Finally, in most of what I was doing at the time, I actually didn't ever need to convert back to a floating point format. All I was going to do with the final price, computed from cost + tax, was just display it to the user...and so if I converted to floating point I'd end up just sprintf'ing it (or equivalent) back to a string. All sprintf would really be being used for was to make sure it had the right padding and leading zeros and stuff like that.
So instead of going through floating point, I just did this (JavaScript):
function cents_to_dollars(cents)
{
cents = cents.toString();
while (cents.length
(I assume that if I knew JavaScript I could do something much nicer than that while look for the padding).