20

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.

Community
  • 1
  • 1
raina77ow
  • 103,633
  • 15
  • 192
  • 229
  • 4
    Well, IE has its own JavaScript engine – Stephen Thomas Oct 03 '13 at 18:10
  • 6
    @StephenThomas: Chrome, Firefox and Opera also have their own JavaScript engines. – Ry- Oct 03 '13 at 18:10
  • 2
    Maybe IE implements `toFixed` using `toString`? It’s not easy to check… – Ry- Oct 03 '13 at 18:13
  • right. so perhaps IE's engine handles FP differently than other browsers – Stephen Thomas Oct 03 '13 at 18:13
  • The algorithm used by [`Number#toFixed`](http://www.ecma-international.org/ecma-262/5.1/#sec-15.7.4.5) is supposed to be standard. Whether a) IE just uses different precision internally to perform this algorithm, b) its implementation has bugs or c) it deviates from the algorithm, is not a question someone can answer without low level analysis or intimate knowledge of IE's source code... – DCoder Oct 03 '13 at 18:13
  • 2
    @DCoder And that's exactly why I asked. ) There's a lot of similar questions here at SO about `toFixed` behaviour of IE6/7; but usually IE is shown in dark colors in those. – raina77ow Oct 03 '13 at 18:18
  • @DCoder: Hmm? A bug is a deviation from specification. There is no difference between “has a bug” and “deviates from the specified algorithm”. (Except the specification only requires “as if” behavior, not exact reproduction of the algorithms.) – Eric Postpischil Oct 03 '13 at 18:22
  • 1
    @EricPostpischil: the difference I was getting at was "algorithm implemented as specified, but there's a bug that was not found" versus "algorithm implemented as we see fit because we know better than the W3C". This *is* IE we're talking about :) – DCoder Oct 03 '13 at 18:24

3 Answers3

8

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”.
Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • So the answer is simply that IE deviates from the ECMA specification with respect to floating point numbers? – Adam Rackis Oct 03 '13 at 18:32
  • 1
    @AdamRackis: My first suspicion would be that `toFixed` and other conversions between binary and decimal deviate from the standard, rather than all floating-point numbers and arithmetic. – Eric Postpischil Oct 03 '13 at 18:35
8

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"
raina77ow
  • 103,633
  • 15
  • 192
  • 229
  • `toFixed` is defined only for the `Number` type. So, even if a numeral is stored as a string for a time, it should be converted to a `Number` before applying `toFixed`. What does `(2.175 - 0).toFixed(20)` produce? Or `(4.35 / 2).toFixed(20)`? If those produce the same output as before, then it is consistent with `toFixed` performing incorrectly. If they produce “2.17499999999999982236”, then it is consistent with storing a plain numeral as a string but converting it to a `Number` when arithmetic is performed (and cheating with a `toFixed` function that operates on strings instead of numbers). – Eric Postpischil Oct 03 '13 at 23:12
  • They both produce 2.175 - and while the first can be written off as a no-op (in theory at least), the second is puzzling indeed. There's something definitely... different (in absense of a better word) with 'toFixed' implementation in IE, I'll update the question with another set of experiments. – raina77ow Oct 04 '13 at 07:02
  • We need to see things from a non-webdeveloper view in this approach. Afterall, `Number` is nothing but a JavaScript class. The actual source code behind those classes in whatever language IE is written in (I'd guess some Microsoft dialect...) is unknown to us. Thus a conversion from string to number may not be required behind the scenes. Maybe IE even "subtracts strings" instead of numbers, converting our arithmetics into strings... still, this would definitely deviate from the IEEE specs and moreover not explain the inconsistency the closer we get to 0. I'm confused... – Kiruse Oct 06 '13 at 12:29
1

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.

Sayem Shafayet
  • 159
  • 1
  • 10