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?