4
0.1 + 0.2
// => 0.30000000000000004

0.2 + 0.2
// => 0.4

0.3 + 0.2
// => 0.5

I understand it has to do with floating points but what exactly is happening here?

As per @Eric Postpischil's comment, this isn't a duplicate:

That one only involves why “noise” appears in one addition. This one asks why “noise” appears in one addition and does not appear in another. That is not answered in the other question. Therefore, this is not a duplicate. In fact, the reason for the difference is not due to floating-point arithmetic per se but is due to ECMAScript 2017 7.1.12.1 step 5

dsp_099
  • 5,801
  • 17
  • 72
  • 128
  • Possible duplicate of [How to deal with floating point number precision in JavaScript?](https://stackoverflow.com/questions/1458633/how-to-deal-with-floating-point-number-precision-in-javascript) – Get Off My Lawn Jun 09 '18 at 20:54
  • 6
    Possible duplicate of [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – Ivan Jun 09 '18 at 20:55
  • Try this: https://www.h-schmidt.net/FloatConverter/IEEE754.html. Notice difference between fields `You entered` and `Value actually stored in float` when you put 0.3, 0.4 and 0.5. –  Jun 09 '18 at 20:55
  • 3
    The value for 0.1+0.2 is **not** unpredictable, it is always the same (but maybe not the one you expected). – gammatester Jun 09 '18 at 20:56
  • [The same question with different numbers](https://stackoverflow.com/questions/50745411/addition-of-floating-point-numbers-in-javascript/50746294#50746294) was asked two days ago. (I am waiting for the incorrect-duplicate close votes to accumulate. If they result in closing the question, I will reopen it and mark it as a duplicate of the proper original.) – Eric Postpischil Jun 09 '18 at 21:40
  • [That question](https://stackoverflow.com/questions/1458633/how-to-deal-with-floating-point-number-precision-in-javascript) is not a correct duplicate. It asks about rounding for computation and/or display, not about the underlying causes or about why some additions appear to cause noise and some do not. – Eric Postpischil Jun 09 '18 at 21:41
  • @Ivan: Please do not promiscuously mark floating-point questions as duplicates of that question. The reason many digits appear in one result and not another as asked about in this question have to do with the JavaScript specification as well as floating-point, so the question is not answered merely by floating-point properties alone. We do not close all questions about JavaScript as duplicates of some “Is JavaScript broken?” question; we answer specific questions with specific answers. Floating-point also has specific behaviors that can be elucidated. – Eric Postpischil Jun 09 '18 at 21:47
  • @EricPostpischil as far as I'm concerned `what exactly is happening here?` means he would like to know more about why this happens. The question asked here is the same as the one asked [there](https://stackoverflow.com/questions/588004/is-floating-point-math-broken). While the latter speaks of floating point in general, the [accepted answer](https://stackoverflow.com/a/588014/6331369) mentions JavaScript. So I still stand by my vote to close this thread as a duplicate. – Ivan Jun 09 '18 at 22:12
  • @Ivan: It is not the same as the one [there](https://stackoverflow.com/questions/588004/is-floating-point-math-broken). That one **only** involves why “noise” appears in one addition. This one asks why “noise” appears in one addition and does not appear in another. That is not answered in the other question. Therefore, this is not a duplicate. In fact, the reason for the difference is not due to floating-point arithmetic *per se* but is due to ECMAScript 2017 7.1.12.1 step 5. – Eric Postpischil Jun 09 '18 at 22:24
  • Fair enough Eric, would you provide an answer to this question? – Ivan Jun 09 '18 at 22:38
  • @EricPostpischil I too would be interested in the details of that section related to this question; I didn't start working it out yet. – Dave Newton Jun 10 '18 at 01:51
  • @Ivan: Done, I posted an answer. – Eric Postpischil Jun 10 '18 at 02:48

2 Answers2

6

When converting Number values to strings in JavaScript, the default is to use just enough digits to uniquely distinguish the Number value.1 This means that when a number is displayed as “0.1”, that does not mean it is exactly 0.1, just that it is closer to 0.1 than any other Number value is, so displaying just “0.1” tells you it is this unique Number value, which is 0.1000000000000000055511151231257827021181583404541015625. We can write this in hexadecimal floating-point notation as 0x1.999999999999ap-4. (The p-4 means to multiply the preceding hexadecimal numeral, by two the power of −4, so mathematicians would write it as 1.99999999999916 • 2−4.)

Here are the values that result when you write 0.1, 0.2, and 0.3 in source code, and they are converted to JavaScript’s Number format:

  • 0.1 → 0x1.999999999999ap-4 = 0.1000000000000000055511151231257827021181583404541015625.
  • 0.2 → 0x1.999999999999ap-3 = 0.200000000000000011102230246251565404236316680908203125.
  • .3 → 0x1.3333333333333p-2 = 0.299999999999999988897769753748434595763683319091796875.

When we evaluate 0.1 + 0.2, we are adding 0x1.999999999999ap-4 and 0x1.999999999999ap-3. To do that manually, we can first adjust latter by multiplying its significand (fraction part) by 2 and subtracting one from its exponent, producing 0x3.3333333333334p-4. (You have to do this arithmetic in hexadecimal. A16 • 2 = 1416, so the last digit is 4, and the 1 is carried. Then 916 • 2 = 1216, and the carried 1 makes it 1316. That produces a 3 digit and a 1 carry.) Now we have 0x1.999999999999ap-4 and 0x3.3333333333334p-4, and we can add them. This produces 4.ccccccccccccep-4. That is the exact mathematical result, but it has too many bits for the Number format. We can only have 53 bits in the significand. There are 3 bits in the 4 (1002) and 4 bits in each of the trailing 13 digits, so that is 55 bits total. The computer has to remove 2 bits and round the result. The last digit, E16, is 11102, so the 10 bits have to go. These bits are exactly ½ of the previous bit, so it is a tie between rounding up or down. The rule for breaking ties says to round so the last bit is even, so we round up to make the 11 bits become 100. The E16 becomes 1016, causing a carry to the next digit. The result is 4.cccccccccccd0p-4, which equals 0.3000000000000000444089209850062616169452667236328125.

Now we can see why printing .1 + .2 shows “0.30000000000000004” instead of “0.3”. For the Number value 0.299999999999999988897769753748434595763683319091796875, JavaScript shows “0.3”, because that Number is closer to 0.3 than any other Number is. It differs from 0.3 by about 1.1 at the 17th digit after the decimal point, whereas the result of the addition we have differs from 0.3 by about 4.4 at the 17th digit. So:

  • The source code 0.3 produces 0.299999999999999988897769753748434595763683319091796875 and is printed as “0.3”.
  • The source code 0.1 + 0.2 produces 0.3000000000000000444089209850062616169452667236328125 and is printed as “0.30000000000000004”.

Now consider 0.2 + 0.2. The result of this is 0.40000000000000002220446049250313080847263336181640625. That is the Number closest to 0.4, so JavaScript prints it as “0.4”.

Finally, consider 0.3 + 0.2. We are adding 0x1.999999999999ap-3 and 0x1.3333333333333p-2. Again we adjust the second operand, producing 0x2.6666666666666p-3. Then adding produces 0x4.0000000000000p-3, which is 0x1p-1, which is ½ or 0.5. So it is printed as “0.5”.

Another way of looking at it:

  • The values for source code 0.1 and 0.2 are both a little above 0.1 and 0.2, respectively, and adding them produced a number above 0.3, with errors that reinforced, so the total error was big enough to push the result away from 0.3 enough that JavaScript showed the error.
  • When adding 0.2 + 0.2, the errors again reinforce. However, the total error in this case is not enough to push the result too far away from 0.4 that JavaScript displays it differently.
  • The value for source code 0.3 is a little under 0.3. When added to 0.2, which is a little over 0.2, the errors canceled, yielding exactly 0.5.

Footnote

1 This rules comes from step 5 in clause 7.1.12.1 of the ECMAScript 2017 Language Specification.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • Thank you for this answer. Do you have any books you can suggest for a mid-level web guy to actually understand the latter part of this? – dsp_099 Jun 10 '18 at 03:20
  • @dsp_099: For the bare bones of floating-point arithmetic, the IEEE-754 1985 standard is not too long and is readable. (The 2008 standard is the one currently in effect, but it is longer and uses somewhat more burdened language. Also, a new draft is in progress.) For a textbook, *Handbook of Floating-Point Arithmetic* by *Muller* *et al* is excellent. For knowing the ins and outs of how different languages and pieces of software handle floating-point conversions and other things, I am not away of any single source—it comes from experience and researching different specifications. – Eric Postpischil Jun 10 '18 at 11:38
  • Thanks for this-saved some time, and I expect it will become another canonical answer. – Dave Newton Jun 10 '18 at 12:04
0

It's not a problem of javascript itself or any other language, but rather a problem of communication between two different races: like human and machine and thinking power of both. What would seem pretty natural to us (like a word: tree- when we say that we create some abstract representation of tree in our head) is totally not natural to computer, and the only thing machine can do to refer to a word "tree" is to store it in some representational way easily understandable by machine (any way you want really, someone many years ago picked a binary code with ASCII table and it seem to be solid though right now). So hereafter the machine has a representation of a word tree stored somewhere there, let's say it's 00000001, but it doesn't know anything beyond that, for you it has some meaning, for a machine it's just a bunch of zeros and one. If we then say that for every word there might be maximum of 7 bits, because otherwise computer works slowly, then machine would save 0000000 cutting last bit and therefor it would still understand the word tree in some way. The same goes with numbers, 0.3 is natural for you, but when you see 10101010001010101010101010111 you would immediately like to convert it to decimal system to understand what it stands for because it's not natural to you to see numbers in binary system. And here comes the main point: conversion.

And thus, for you math looks like this:

.1 + .2 => .3

For a machine that uses binary system looks like:

The number 1/10 can be expressed as 0.1 in decimal, but it is 0.0001100110011001100110011001100110011001100110011001….. in binary. Because there's only 53 bit space for a number according to standard, starting from bit 54, the number will be rounded.

x = .1 converted to 00011001...01 and trimmed to 53 bits

y = .2 converted to 00010110...10 and trimmed to 53 bits

z = x + y => 0001100010101... trimmed to 53 bits

result = 0.3000000000000000444089209850062616169452667236328125 converted from z = 0001100010101...

It's like converting euro to dollar, sometimes you will gain a half of a cent and sometimes you will pay a half of a eurocent more for a dollar representation because there is no smaller piece than cent. There might be, but people would get insane with their pockets.

So the real question is Why does 0.1 converted to binary and trimmed + 0.2 converted to binary and trimmed return unpredictable float results in JavaScript while 0.2 converted to binary and trimmed + 0.3 converted to binary and trimmed does not? and the answer is: because of math and an amount of power given for calculations, analogous to why pi + 1 gives strange result but 2 + 1 does not => you probably putted some representation of pi like 3.1415 because you don't have enough mathematical power (or it's not worth) to make an exact result.

To read more, great piece of math is done here: https://medium.com/dailyjs/javascripts-number-type-8d59199db1b6

Maciej Kwas
  • 6,169
  • 2
  • 27
  • 51