The tax rate (like most things associated with money) should be stored as an exact decimal (fixed point decimal, decimal floating point, or arbitrary precision decimal all work.)
> I guess you need to calculate the tax for each item, round it to cents (and I assume the rules on how you round can vary by jurisdiction, so it's not just calling your language's round function), multiply that by the quantity of that item (good luck if the quantity can be a decimal value), then sum each line item.
Usually taxes are required to be calculated by summing the pre-tax cost of all line items, applying the tax rate, rounding by the applicable rules, and then adding that to the pre-tax subtotal to get the final total. But having decimal unit prices (even decimal cents, e.g., gas is usually priced in mils, not cents), decimal quantities, and decimal tax rates are all non-problems (so no "good luck" needed) as long as you are using an exact decimal representation rather than binary floating point.
In Italy the generic VAT rate is 22%.
When you expose a price (to the final customer) it is always included VAT, but VAT needs to be explicited on fiscal documents (receipt and/or invoices).
So you have an article priced to the public Euro 2,69 (included 22% VAT), what is the net price and how much is the VAT? 2,69/1,22=2,204918...
So, let's try rounding it to 2,20: 2,20x0,22=0,484=0,48 no, wait, 2,20+0,48=2,68
Ok, let's try rounding it to 2,21: 2,21x0,22=0,4862=0,49 no, wait, 2,21+0,49=2,70
You have to use the first and add 0,01 to the VAT to have it work:
2,20+(0,48+0,01)= 2,69
Great, how do you become confident that your hand-rolled decimal type is less buggy than the known hazards of using floating-point?
C#, Ruby, Python, and Java, at least, among industrially popular languages all provide a fixed and/or arbitrary precision decimal type in either the core language or stdlib. Lower level languages sometimes don't, and JavaScript, among higher-level popular languages, notably does not.
In C:
unsigned long tax_in_cents(double amt, double rate)
{
/* requirement: amt is an integer multiple of 0.01
and rate is an integer multiple of 0.00001 */
const unsigned long M = 10000000;
return (unsigned long)round((round(amt * rate * M) / (M/100)));
}
That should work, at least for amt that isn't too high.Here is how I would do it, using integers:
unsigned long tax_in_cents(unsigned long amt2, unsigned long rate5)
{
/* amt2 is the amount in cents, rate5 is tax rate * 10000 */
return (amt2 * rate5 + 50000) / 100000;
}
(My convention in this kind of code is that if an underlying thing is represented as an integer by multiplying it by a power of 10, I put that power as a suffix on the integer variable name. So amt2 means the real amount x 100, and rate5 means the tax rate x 100000. A ...2 x ...5 gives something that is ...7, which is why we divide by 10^5 to get the tax in cents (a ...2). Half of the divisor, or 10^5/2, is what you add for rounding).I've not proven the above floating point code, but I have brute force tested it with every combination of rates from 0.00001 to 0.99999, price from $0.01 to $10000.00, and floating rounding modes FE_UPWARD, FE_TONEAREST, and FE_DOWNWARD.
Most initial attempts to write such a function make the mistake of doing the rounding at the wrong place. They just try something like amt x rate then round that to the nearest cent.
The problem with that can be seen by a very simple example: 10% of $21.15. It should be $2.12. It comes out before rounding as 2.1149999999999998, so rounding to the nearest 0.01 comes out as $2.11. Various "obvious" ways you might do this might work for that case, but they fail in others. For example, here are three ways in Python that readily come to mind:
def tax_f1(amt, rate):
tax = round(amt * rate,2)
return round(tax * 100)
def tax_f2(amt, rate):
return round(amt*rate*100)
def tax_f3(amt, rate):
return round(amt*rate*100+.5)
but try all of those on these: 1% of $21.50
3% of $21.50
6% of $21.50
10% of $21.15
and every one of them fails at least once. The correct answers 22, 65, 129, and 212. Here is what they give, with the wrong ones prefixed with !: tax_f1: !21 65 129 !211
tax_f2: 22 !64 129 !211
tax_f3: 22 65 !130 212
There was a discussion of this here a couple months ago [1]. In that discussion, someone who knows considerably more about floating point that I do (kccqzy) suggested how to fix it, and even his first try failed when I run it through my brute force testing. He then suggested a method that worked (at least according to my brute force testing).