Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Number.prototype.toFixed: amazingly correct in Internet Explorer

Consider the following:

var x = 2.175;
console.log(x.toFixed(2));  // 2.17

What? No, no surprise here. That's rather obvious, see: Number literal 2.175 is actually stored in memory (by IEEE-754 rules) as a value that's just a tiny little bit smaller than 2.175. And that's easy to prove:

console.log(x.toFixed(20)); // 2.17499999999999982236

That's how it works in the latest versions of Firefox, Chrome, and Opera on 32-bit Windows setup. But that's not the question.

The real question is how Internet Explorer 6 (!) actually manages to do it right as humans do:

var x = 2.175;
console.log(x.toFixed(2));  // 2.18
console.log(x.toFixed(20)); // 2.17500000000000000000

OK, I overdramatized: actually all Internet Explorers I tested this on (IE8-11, and even MS Edge!) behave the same way. Still, WAT?

UPDATE: It gets stranger:

x=1.0;while((x-=0.1) > 0) console.log(x.toFixed(20));

IE                        Chrome
0.90000000000000000000    0.90000000000000002220
0.80000000000000000000    0.80000000000000004441
0.70000000000000010000    0.70000000000000006661
0.60000000000000010000    0.60000000000000008882
0.50000000000000010000    0.50000000000000011102
0.40000000000000013000    0.40000000000000013323
0.30000000000000015000    0.30000000000000015543
0.20000000000000015000    0.20000000000000014988
0.10000000000000014000    0.10000000000000014433
0.00000000000000013878    0.00000000000000013878

Why the difference - in all but the last one? And why no difference in the last one? It's very similar for x=0.1; while(x-=0.01)..., by the way: until we get very close to zero, toFixed in IE apparently attempts to cut some corners.

Disclaimer: I do know that floating-point math is kinda flawed. What I don't understand is what's the difference between IE and the rest of the browser's world.

like image 803
raina77ow Avatar asked Oct 03 '13 18:10

raina77ow


People also ask

What does toFixed() do?

The toFixed() method converts a number to a string. The toFixed() method rounds the string to a specified number of decimals.

Does toFixed round up?

toFixed() returns a string representation of numObj that does not use exponential notation and has exactly digits digits after the decimal place. The number is rounded if necessary, and the fractional part is padded with zeros if necessary so that it has the specified length.


3 Answers

The reported behavior deviates from the requirements of the ECMA specification.

Per clause 8.5, the Number type has the IEEE-754 64-bit binary values, except there is only one NaN. So 2.175 cannot be represented exactly; the closest you can get is 2.17499999999999982236431605997495353221893310546875.

Per 15.7.4.5, toFixed(20) uses an algorithm that boils down to:

  • “Let n be an integer for which the exact mathematical value of n ÷ 10fx is as close to zero as possible. If there are two such n, pick the larger n.”
  • In the above, f is 20 (the number of digits requested), and x is the operand, which should be 2.17499999999999982236431605997495353221893310546875.
  • This results in selecting 217499999999999982236 for n.
  • Then n is formatted, producing “2.17499999999999982236”.
like image 165
Eric Postpischil Avatar answered Oct 17 '22 07:10

Eric Postpischil


I appreciate Eric's contribution, but, with all due respect, it doesn't answer the question. I admit I was too tongue-in-cheeky with those 'right' and 'amazingly correct' phrases; but yes, I understand that IE behavior is a deviation actually.

Anyway. I was still looking for an explanation what causes IE to behave differently - and I finally got something looking like a clue... ironically, in Mozilla's tracker, in this lengthy discussion. Quote:

OUTPUT IN MOZILLA: 
a = 0.827 ==> a.toFixed(17) = 0.82699999999999996 
b = 1.827 ==> b.toFixed(17) = 1.82699999999999996

OUTPUT IN IE6: 
a = 0.827 ==> a.toFixed(17) = 0.82700000000000000 
b = 1.827 ==> b.toFixed(17) = 1.82700000000000000

The difference seen in IE and Mozilla is as follows. IE is storing 'a' as a string and Mozilla is storing 'a' as a value. The spec doesn't nail down the storage format. Thus when IE does a.toFixed it starts out with a exact string representation while Mozilla suffers the round trip conversions.

Would be great to have kind of official confirmation on this, but at least that explains everything I have seen yet. In particular,

console.log( 0.3.toFixed(20) ); // 0.30000000000000000000
console.log( 0.2.toFixed(20) ); // 0.20000000000000000000
console.log( (0.3 - 0.2).toFixed(20) ); // "0.09999999999999998000"
like image 42
raina77ow Avatar answered Oct 17 '22 09:10

raina77ow


First of all, Floating points are can not be "precisely" represented in binary numbers. There will be an elevation/depression, either the value will be a little higher or a little lower. How much that is elevated/depressed depends on how the conversion is done. There is not exactly a "right value" even for a string output of ECMAScript's toFixed().

But the ECMA Standards do spice things up by er.. setting standards. Which is a good thing in my opinion. It's like "If we are all gonna make mistakes anyway, let's make the same one."

So, the question now would be, how and why does IE deviates from the Standards. Let's examine the following test cases.

Candidates are IE 10.0.9200.16688 and Chrome 30.0.1599.69, running on x64 Windows 8 Pro.

Case Code                       IE (10)                        Chrome (30)
--------------------------------------------------------------------------------
A    (0.06).toFixed(20)         0.60000000000000000000    0.05999999999999999778
B    (0.05+0.01).toFixed(20)    0.06000000000000000500    0.06000000000000000472

So, regardless it's IE or Chrome, we see (0.06) is not exactly equal to (0.05+0.01). Why is that? It's because (0.06) has a representation that is very close to but not equal to (0.06), so does (0.05) and (0.01). When we perform an operation, such as an addition, the very less significant errors can sum up to become an error of slightly different magnitude.

Now, the difference in represented value in different browsers can be impacted due to 2 reasons:

  • The conversion algorithm used.
  • When the conversion takes place.

Now we don't know what algo IE uses since I can't look into it's source. But the above test cases clearly demonstrate one other thing, IE and Chrome handles the conversion "not only differently" but also "on a different occasion".

In JavaScript, when we create a number (aka instance of a Number class with or without the new keyword), we actually provide a literal. The literal is always a string even if it denotes a number [1]. The browser parses the literal and creates the object and assigns the represented value.

Now, here's where things tend to go different ways. IE holds off the conversion until it is needed. That means until an operation takes place, IE keeps the number as literal (or some intermediary format). But Chrome converts it rightaway in the operational format.

After the operation is done, IE does not revert back to the literal format or the intermediary format, as it is pointless and may cause a slight loss in precision.

I hope that clarifies something.


[1] Value represented in code are always literals. If you quote them they are called String Literals.

like image 30
Sayem Shafayet Avatar answered Oct 17 '22 08:10

Sayem Shafayet