How does JavaScript runtime convert BINARY (Double-precision floating-point format) back to DECIMAL

Viewed 276

Give a decimal number 0.2

EX

var theNumber= 0.2;

I ASSUME it would be stored in memory as (based on double-precision 64-bit floating point format IEEE 754)

0-01111111100-1001100110011001100110011001100110011001100110011001

That binary number is actually rounded to fit 64 bit.

If we take that value and convert it back to decimal, we will have

0.19999999999999998

(0.1999999999999999833466546306226518936455249786376953125)

Not exactly 0.2

My question is, when we ask for decimal value of theNumber (EX: alert(theNumber)), how does JavaScript runtime know theNumber is originally 0.2?

3 Answers

JavaScript’s default conversion of a Number to a string produces just enough decimal digits to uniquely distinguish the Number. (This arises out of step 5 in clause 7.1.12.1 of the ECMAScript 2018 Language Specification, which I explain a little here.)

Let’s consider the conversion of a decimal numeral to a Number first. When a numeral is converted to a Number, its exact mathematical value is rounded to the nearest value representable in a Number. So, when 0.2 in source code is converted to a Number, the result is 0.200000000000000011102230246251565404236316680908203125.

When converting a Number to decimal, how many digits do we need to produce to uniquely distinguish the Number? In the case of 0.200000000000000011102230246251565404236316680908203125, if we produce “0.2”, we have a decimal numeral that, when again converted to Number, the result is 0.200000000000000011102230246251565404236316680908203125. Thus, “0.2” uniquely distinguishes 0.200000000000000011102230246251565404236316680908203125 from other Number values, so it is all we need.

In other words, JavaScript’s rule of producing just enough digits to distinguish the Number means that any short decimal numeral when converted to Number and back to string will produce the same decimal numeral (except with insignificant zeros removed, so “0.2000” will become “0.2” or “045” will become “45”). (Once the decimal numeral becomes long enough to conflict with the Number value, it may no longer survive a round-trip conversion. For example, “0.20000000000000003” will become the Number 0.2000000000000000388578058618804789148271083831787109375 and then the string “0.20000000000000004”.)

If, as a result of arithmetic, we had a number close to 0.200000000000000011102230246251565404236316680908203125 but different, such as 0.2000000000000000388578058618804789148271083831787109375, then JavaScript will print more digits, “0.20000000000000004” in this case, because it needs more digits to distinguish it from the “0.2” case.

In fact, 0.2 is represented by other bit sequence than you posted.
Every time your result will match correct bit sequence, console will output 0.2. But if your calculation results in other sequence, console will output something like your 0.19999999999999998.

Similar situation is with most common example 0.1 + 0.2 which gives output 0.30000000000000004 because bit sequence for this result is different than in 0.3's representation.

console.log(0.2)
console.log(0.05 + 0.15)
console.log(0.02 + 0.18)

console.log(0.3)
console.log(0.1 + 0.2)
console.log(0.05 + 0.25)

From ECMAScript Language Specification:

11.8.3.1 Static Semantics: MV
A numeric literal stands for a value of the Number type. This value is determined in two steps: first, a mathematical value (MV) is derived from the literal; second, this mathematical value is rounded [...(and here whole procedure is described)]

You may be also interested in following section:

6.1.6 Number type
[...]
In this specification, the phrase “the Number value for x” where x represents an exact real mathematical quantity [...] means a Number value chosen in the following manner.
[...(whole procedure is described)]
(This procedure corresponds exactly to the behaviour of the IEEE 754-2008 “round to nearest, ties to even” mode.)

So, my ASSUMPTION is wrong.

I have written a small program to do the experiment.

The binary value that goes to memory is not

0-01111111100-1001100110011001100110011001100110011001100110011001

The mantissa part is not 1001100110011001100110011001100110011001100110011001

It got that because I truncated the value, instead of rounding it. :((

1001100110011001100110011001100110011001100110011001...[1001] need to be rounded to 52 bit. Bit 53 if the series is a 1, so the series is rounded up and becomes: 1001100110011001100110011001100110011001100110011010

The correct binary value should be:

0-01111111100-1001100110011001100110011001100110011001100110011010

The full decimal of that value is:

0.200 000 000 000 000 011 102 230 246 251 565 404 236 316 680 908 203 125

not

0.199 999 999 999 999 983 346 654 630 622 651 893 645 524 978 637 695 312 5

And as Eric's answer, all decimal numbers, if are converted to the binary

0-01111111100-1001100110011001100110011001100110011001100110011010

will be "seen" as 0.2 (unless we use toFixed() to print more digits); all those decimal numbers SHARE the same binary signature (i really don't know how to describe it).

Related