0

So due to the fact that product quantities are not limited to integers but can store decimals up to 4 digits, I can have a product with quantity of 69.4160 square meters of something.

Now assuming the currency is PLN (but I think it doesn't matter) and tax rate is 23%, my cart calculates subtotals and totals for the following products correctly.

Product A: costs 129.00 * 69.4160 = 8954.6640 (11014.2367 with tax) Product B: costs 150.00 * 1 = 150.0000 (184.5000 with tax) 

These calculations are correct, but for humans I want to display prices up to 2 decimal places hence I call price_float_to_money_format() for cart subtotal, tax and total.

This is my method:

if (!function_exists('price_float_to_money_format')) { function price_float_to_money_format($price): string { // return $price; $formatter = new NumberFormatter('pl_PL', NumberFormatter::CURRENCY); $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_UP); // up seems wrong // $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_DOWN); // or down which seems OK? $internationalCurrencySymbol = $formatter->getSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL); return $formatter->formatCurrency($price, $internationalCurrencySymbol); } } 

The following gives me nicely looking prices.

If my subtotal is 9104.6640 it gives me 9 104,67 zł, for tax of 2094.0727 I get 2 094,08 zł but for the whole total of 11198.7367 I get 11 198,74 zł ‼️ when I NumberFormatter::ROUND_UP.

However when I use NumberFormatter::ROUND_DOWN const I get 11 198,73 zł (it also rounds down subtotal from 9 104,67 zł to 9 104,66 zł).

Please help me understand how should I round as in this particular case I think rounding down is correct, because when I manually sum on calc it shows 73 "cents" rather than 74.

enter image description here

Where do I make mistake?

10
  • There is quite a bit of freedom in how you round. The only thing of interest to the tax-man is the final total (per VAT tariff) and the VAT you charge over that. That has to match the VAT tariff. In most tax places you are allowed to round down the VAT, but for your own sanity it might be better to use something like NumberFormatter::ROUND_HALFEVEN because it will balance out up and down roundings over multiple numbers. Commented May 14 at 10:37
  • In general I would suggest using bcmath when dealing with currency. Even better, use only full integers the whole time and display only float (for example by dividing by hundred). Commented May 14 at 11:45
  • @MarkusZeller He can't use integers the whole time if the quantity might be a value like 69.4160 square meters of something. Commented May 14 at 11:47
  • @MattKomarnicki It looks to me like the issue here is whether you add up the raw values and then round, or add up the rounded/formatted values. To me summing the rounded values seems wrong, as it can lead to lots of anomalies — it is basically an example of double rounding. (But with that said, even if you sum the raw values and then round, there are still some anomalies that can creep in, due to binary vs. decimal arithmetic.) Commented May 14 at 11:52
  • 1
    Another solution is to calculate the total, round each of the summands, and then adjust one (or more) of the summands. The latter introduces some issues with symmetries. For example, if you have three items each priced at ⅔, rounding will make each of them .66 or each of them .67, depending on the rounding rule, but the rounded total is 2.00, and there is no way to make the rounded prices of the three identical while the total is 2.00. One solution to that is to charge each item at the round-up price until the accumulated discrepancy reaches a unit: .67 + .67 + .66 = 2.00. Commented May 14 at 15:18

4 Answers 4

1

When you force‐round every number "up" or "down", it introduces minor mismatches in your case. You have to drop this and use ROUND HALF UP

function price_format(float $price): string { $formatter = new NumberFormatter('pl_PL', NumberFormatter::CURRENCY); $formatter->setAttribute( NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_HALFUP ); return $formatter->formatCurrency($price, 'PLN'); } 

Output

# Calculate raw $rawSubtotal = 9104.6640; # 9104.6640 $rawTax = 2094.0727; # 2094.07272 $rawTotal = 11198.73672; # 11198.73672 # Format half-up echo 'Subtotal: ' . price_format($rawSubtotal) . "\n"; # 9 104,66 zł echo 'Tax (23%): ' . price_format($rawTax) . "\n"; # 2 094,07 zł echo 'Total: ' . price_format($rawTotal) . "\n"; # 11 198,74 zł 

Preview


Or

function format_pln(float $price): string { $rounded = round($price, 2, PHP_ROUND_HALF_UP); return number_format($rounded, 2, ',', ' ') . ' zł'; } 

Output

echo format_pln(9104.6640); # 9 104,66 zł echo format_pln(2094.0727); # 2 094,07 zł echo format_pln(11198.7367); # 11 198,74 zł 

Preview

Sign up to request clarification or add additional context in comments.

5 Comments

Your example adds that extra cent when I sum 66 cents (from subtotal and 7 from tax). 66 + 7 is 73 not 74, so I'm worried that client will see that extra cent and won't be happy. This is why I round down to get 73. Also for payment provider when I have to convert 11198.7367 to integer using bcmul it correctly gives 1119873 rather than 1119874. Thoughts?
ROUND_HALFUP does not fix the problem. Given three items of cost .333, with a total .999, rounding would make each item .33, which sum to .99, but rounding the total makes it 1.00.
NumberFormatter::ROUND_DOWN seems to be correct rounding mode.
@MattKomarnicki: ROUND_DOWN does not fix the problem either. Given three items of cost .666, with a total 1.998, rounding down would make each item .66, which sum to 1.98, but rounding the total makes it 2.00.
There is no rounding function r such that sum(r(p[i])) = r(sum(p[i])) of all possible sets of prices p[i]. (A rounding function is one that maps real numbers to multiples of a unit such that x is mapped to x if it is a multiple and, otherwise, x is mapped to one of the two nearest multiples.) This is because there is no way for a single-operand rounding function to account for how the fractions of units add up. The solution is that you cannot use a rounding function to accomplish OP’s desires. You could round all the individual items and then calculate their sum.
1

You're dealing with a fundamental issue in financial rounding: individual rounding vs. cumulative rounding.

Key points:

  • round(a) + round(b) ≠ round(a + b) In many cases. This is expected and unavoidable.

  • Always perform calculations in full precision. Only round once, at the final step, when displaying, storing to DB, or sending to payment systems.

  • Use deterministic rounding strategies:

    • ROUND_HALF_UP It is standard for most currencies.

    • Avoid always rounding up/down, this introduces bias.

    • Use ROUND_DOWN only when legally required (e.g. specific tax laws).

  • Use BCMath or a similar high-precision library for all internal calculations. Define precision per currency, e.g. USD should always enter, process, and leave your system with exactly two decimal places.

  • For invoices or receipts, if totals don’t align due to rounding, adjust the last line item to match the correct total.

1 Comment

You can use the brick/money library for handling money and different currencies, it manages currency-specific precision and rounding. If you're dealing with just one currency, BCMath is simpler and works well.
1

Iʼll generalize it a bit: In the way you initially tried it, there is no method that produces exact equations in all cases. This is a common problem.

I compare it to what I had met in my program, in Ukraine. The common VAT is 20%. Imagine an item that costs 10.12 (no matter, złoti.grosze, gryvni.kopijky, etc.), you sell all items of this price but with different names.

Rounding half-up (the most probable): VAT of one item is 2.02; VAT of double item is 4.05; VAT of two separate items is 4.04. Oops.

Rounding down: VAT of one item is 2.02; VAT of double item is 4.04; VAT of triple item is round_down(30.36) == 6.07, not 6.06. Oops.

Rounding up: VAT of one item is 2.03; VAT of double item is 4.05, not 4.06. Oops.

With your 23%, the effect may emerge even earlier.

Or, let you deal with VATful prices in the database. An item costs 12.99. The 20% VAT is 2.165... but how to round it - 2.16 or 2.17? For both variants, if to calculate VATful price back from VATless one, using half-up or half-even rounding, there will be an error for 0.01: 10.82 -> 12.98, but 10.83 -> 13.00. Using down or up rounding, other errors will emerge.

In my case, there was a long period of indetermination, how to calculate, but finally the state tax service issued an instruction to calculate VAT over a total invoice, after summing all items of all types.

(UPD: A neighbor answer contains a concise phrase: "individual rounding vs. cumulative rounding". These terms shall be recorded. But the entire problem is not limited to them.)

This is not a technical (programming) problem on its own. This is an administrative policy problem. Finances are calculated in the established currency with the defined accuracy using the established rules. You have to follow the regulations defined for your target domain - Poland in your case.

In your case, the algorithm should be:

​1. Collect the rules applied at the target domain (Poland), how VAT is calculated. You know the rate but you should also discover the rounding method. Iʼm certain for ≈95% it is rounding to 1 grosz with half_up mode, but let you verify. Then, find the rule for a total invoice as well. Per-item, per-position (a set of identical items), per-invoice - whatever.

​2. Define what price - VATful or VATless - shall be the base stored in database. This is not quite simple, the gross and the retail trade may require different decisions.

​I"m also almost certain your intermediate approach with keeping 1/100 of a grosz doesnʼt fit to the rules. It has a sense if you calculate, for example, how a piece of cheese costs if it weighs 757 grams and the price is €9.22 per kg... but, after multiplication, the item price shall be rounded to 1 grosz, and all further calculations shall be based on this price. Again, there are state regulations, merely follow them.

3. For each item, when a VATless price is stored, calculate VATful price and show difference between the former and the latter as per-price VAT. Or vice versa, if you keep VATful prices. But at the instant, the equation VATless+VAT==VATful for the values you show to a use shall be correct, otherwise you will be beaten by aggressive fraud allegations.

​4. Calculate final per-invoice VAT as your state tax service requires, and provide a note for users with (a concise and easy to understand) note about possible minor mismatch between per-item VAT sum and final VAT.

Comments

0

calculate the total using raw values, format the total from the raw value.

calculate back the formatted price and VAT from the formatted total.

$basePrice = 9104.6640; $vatRate = 0.23; // calculate VAT $vatAmount = $basePrice * $vatRate; // calculate total price including VAT $totalPrice = $basePrice + $vatAmount; $formattedTotal = number_format($totalPrice, 2); // or whatever formatting you want $formattedPrice = number_format($basePrice, 2); $formattedVat = floatval(preg_replace('/[^\d.]/', '', $formattedTotal)) - floatval(preg_replace('/[^\d.]/', '', $formattedPrice); $formattedVat = number_format($formattedVat, 2); echo $formattedPrice . "\n"; // 9,104.66 echo $formattedVat . "\n"; // 2,094.08 echo $formattedTotal . "\n"; // 11,198.74 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.