1

I think I tried all the examples from the net! This is my PHP code:

echo(number_format(22.95 * 4.1, 2));

The result is 94.10 with PHP. I have been trying to get the same result with JavaScript, but no matter what I try, its always 94.09. Any idea how to make that work?

Some of the code I tried:

var x = 22.95;
var y = 4.10;
var res = x * y;
var res1 = res.toFixed(3);
console.log(res);
console.log(res1);
console.log(Number(res1).toFixed(2));
console.log(Math.round(res * 100) / 100);

Thanks!

user4095519
  • 271
  • 1
  • 5
  • 12

2 Answers2

2

PHP number_format() does more than JS Number.prototype.toFixed(), therefore, you can't use it as a stand-in.

And that is apart from having parameters for the symbols decimal and large number separators in front of and including the radix point.

Not to mention that you're probably ending in porting a function (Cf. What is the JS equivalent to the PHP function number_format?). Hint: Give them your number as a new test-case.

For 94.10 vs. 94.09, this is Q&A-ed in Is floating point math broken?, and as we already have identified that rounding is missing in Javascript, the problem statement is:

> 22.95 * 4.1 != 94.095
true

Same for PHP btw.:

> 22.95 * 4.1 != 94.095
= true

Makes sense, .5 in the last place is a number exactly between two numbers in base 2.

Now the question is, if it's still possible to communicate the number to format for display with enough of precision so that any floating point calculations Number.prototype.toFixed() applies internally don't add up floating point precision errors that much that there is too little precision left for displaying its approximation with two digits?

Reading that number_format() is "using the rounding half up rule", rounding¹ before passing it to toFixed(2) may already suffice for your use-case.

A precision of three digits after the radix point with "rounding if necessary" should suffice for displaying two digits after the radix point:

> Math.round((22.95 * 4.10).toFixed(3) * 100) / 100
94.1

This number should have enough of too little precision errors so that the string formatting of Number.prototype.toFixed() for two digits still formats the string correctly:

> (Math.round((22.95 * 4.10).toFixed(3) * 100) / 100).toFixed(2)
'94.10'

Yes, it worked. Luckily! As n / 100 again adds floating point precision errors before it gets passed to toFixed(2) which adds more errors to it!

Perhaps:

/**
 * @param {number} num
 * @param {number} decimals natural number of digits (incl. zero) after the radix point
 * @return {string} rounded num using round up rule with decimals after the radix point
 */
function roundUpRule(num, decimals = 0) {
    let factor = num < 0 ? -1 : 1;
    return (Math.round(parseFloat((num * factor).toFixed(1+decimals)) * `1E+${decimals}`) / `1E+${decimals}` * factor).toFixed(decimals);
}

Or as Edward wrote in Nov 2016:

After correctly rounding with the decimal place shifting and rounding method, you could use the number.toFixed(x) method to convert it to a string with the required amount of zeroes. E.g. round 1.34 to 1.3 with the cross-browser method then add 1 zero and convert to string with 1.3.toFixed(2) (to get "1.30")

If this dirty shortcut doesn't suffice, last resort is to apply the rounding rule just on the string, for example:

> (22.95 * 4.10).toFixed(62)
'94.09499999999998465227690758183598518371582031250000000000000000'
'94.0949999999999846522769075818359851837158203125000000000000000'
'94.094999999999984652276907581835985183715820312500000000000000'
'94.09499999999998465227690758183598518371582031250000000000000'
'94.0949999999999846522769075818359851837158203125000000000000'
'94.094999999999984652276907581835985183715820312500000000000'
'94.09499999999998465227690758183598518371582031250000000000'
'94.0949999999999846522769075818359851837158203125000000000'
'94.094999999999984652276907581835985183715820312500000000'
'94.09499999999998465227690758183598518371582031250000000'
'94.0949999999999846522769075818359851837158203125000000'
'94.094999999999984652276907581835985183715820312500000'
'94.09499999999998465227690758183598518371582031250000'
'94.0949999999999846522769075818359851837158203125000'
'94.094999999999984652276907581835985183715820312500'
'94.09499999999998465227690758183598518371582031250'
'94.0949999999999846522769075818359851837158203125'
'94.094999999999984652276907581835985183715820313'
'94.09499999999998465227690758183598518371582031'
'94.0949999999999846522769075818359851837158203'
'94.094999999999984652276907581835985183715820'
'94.09499999999998465227690758183598518371582'
'94.0949999999999846522769075818359851837158'
'94.094999999999984652276907581835985183716'
'94.09499999999998465227690758183598518372'
'94.0949999999999846522769075818359851837'
'94.094999999999984652276907581835985183'
'94.09499999999998465227690758183598518'
'94.0949999999999846522769075818359852'
'94.094999999999984652276907581835985'
'94.09499999999998465227690758183599'
'94.0949999999999846522769075818360'
'94.094999999999984652276907581836'
'94.09499999999998465227690758184'
'94.0949999999999846522769075818'
'94.094999999999984652276907582'
'94.09499999999998465227690758'
'94.0949999999999846522769076'
'94.094999999999984652276908'
'94.09499999999998465227691'
'94.0949999999999846522769'
'94.094999999999984652277'
'94.09499999999998465228'
'94.0949999999999846523'
'94.094999999999984652'
'94.09499999999998465'
'94.0949999999999847'
'94.094999999999985'
'94.09499999999999'
'94.0950000000000'
'94.095000000000'
'94.09500000000'
'94.0950000000'
'94.095000000'
'94.09500000'
'94.0950000'
'94.095000'
'94.09500'
'94.0950'
'94.095'
'94.10'

Given this is needed, then refactor roundUpRule().


¹ Cf. How to round to at most 2 decimal places, if necessary

Discussion

Not only is the decimal numeral system not able to represent all numbers, when you prepare results of calculations with a limited number of decimals for display, e.g. two as in your question, you also need to do rounding already quite often if not always.

number_format() as a function for display purposes does this rounding already.

As you're doing the calculations on a computer system, and you write down the numbers as decimals, it is already that the computer needs to translate them into its own binary numeral system, that again, as every other numeral system, is not able to represent all numbers. And all this is only to do the floating point calculation which is with errors, and they can even add up, loosing precision beyond the precision you need for display.

In mathematics 0.9 "denotes the repeating decimal consisting of an unending sequence of 9s after the decimal point", so 1.0 and 0.9 "represent exactly the same number." (0.999... Wikipedia)

Now for your Javascript in decimal numeral system string representation, it is easy to create a similar impression:

> (22.95 * 4.1).toFixed(14).slice(0, -1)
'94.0949999999999'
> (22.95 * 4.1).toFixed(13)
'94.0950000000000'

Number.prototype.toFixed() is even kind enough to add the repeating zeroes (compare w/ WP again).

But this is only an approximation, and that static method only rounds "when necessary" (naturally for its own operation, not for your numbers and your display purpose).

> (22.95 * 4.1).toFixed(4).slice(0, -1)
'94.095'
> (22.95 * 4.1).toFixed(3)
'94.095'
> (22.95 * 4.1).toFixed(2)
'94.09' # not 94.10

As numbers are stored in a bit field, they can only be stored to the power of two; the rounding error then is in the unit in the last place.

Therefore, it is important you remove this margin of error before it can add up. Then you can apply the string formatting with less error.

Similar as required for the display purposes of numbers in the decimal numeral system, you need to do the rounding. Number.prototype.toFixed() doesn't round for you (only for itself):

(Math.round(229.5 * 41) / 100).toFixed(2)
'94.10'

All this is naturally already outlined within existing Q&A (Cf. Is floating point math broken?) having exactly your problem (only with different values):

22.95 * 4.1 != 94.095

So when you perform floating point arithmetics, design for error and always encode (write) numbers so that you communicate the value with the precision you need well to the methods you make use of.

There is even an existing Q&A about how to do PHP number_format() in Javascript² with code copied from the phpjs project (part of locutus nowadays), but your way to express numbers cracks it. This shows how easy it is to forget that number_format() is rounding and therefore handles floating point precision errors for display purposes already different compared to Number.prototype.toFixed().


² What is the JS equivalent to the PHP function number_format?

hakre
  • 193,403
  • 52
  • 435
  • 836
  • Sorry, but I don't get your answer. Floating-point math is not the same in PHP and JS. Just emulate `console.log(22.95*4.1)` and [`echo 22.95*4.1;`](https://onlinephp.io/). And what exactly is the difference in both rounding functions? "PHPs' number_format() rounds "half up" while (....)" JS also rounds half up?! – Niklas E. Jul 04 '23 at 13:44
  • Node20 tells: 22.95 * 4.1 94.09499999999998 and PHP 7.4 the same (precision of 16). The difference is that toFixed() only rounds when necessary, that is to the precision, it does not take more into account, even the 64bit would/could give that. So it is not mathematically rounding at all one could say? In any case, number_format() does round, not only reducing the precision. And yes, there is a mistake about that in the answer I have to fix that. The _"rounding if necessary"_ of toFixed() is likely what falls into the floating point precision errors as when done that way, it is incomplete. – hakre Jul 04 '23 at 13:56
  • @NiklasE.: You were also interested in the differences of the rounding functions. number_format() is _"using the rounding half up rule"_, Number.prototype.toFixed() is _"rounding as necessary"_ in detail described in https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.prototype.tofixed - how I understand it, this is not the same rounding. – hakre Jul 30 '23 at 10:24
  • Oh, that's very interesting. I also didn't expect `(1000000000000000100).toFixed(0)` to return `"1000000000000000128"` /while `(1000000000000000128).toString()` returns `"1000000000000000100"`)... .toFixed(...) to use the literal binary translation and not the adjacent decimal number logic common in JS. – Niklas E. Jul 30 '23 at 11:32
0

You experiencing a floating point error on the JavaScript side.

22.95*4.1 is not 94.095 (as you might expect) but 94.09499999999998 in (32bit) floating-point math, therefor rounded down to .09 and not up to .10.

There's no good way to fix this in JavaScript but you can force PHP to behave like JavaScript using ini_set('precision', 17).

See answer from question Why can PHP calculate 0.1 + 0.2 when other languages fail?:

PHP has a precision configuration value which sets the number of significant digits displayed in floating point numbers. It is 14 by default, which is the reason 0.1 + 0.2 is displayed as 0.3.

If, however, you do this:

ini_set('precision', 17);
echo 0.1 + 0.2;

you get 0.30000000000000004

Niklas E.
  • 1,848
  • 4
  • 13
  • 25
  • Just fyi: Not sure its floating point precision, a conclusion in [my answer](https://stackoverflow.com/a/76612437/367456) is a different rounding in _number_format()_, not floating-point number format in general between those two languages. – hakre Jul 04 '23 at 12:28