Here-is-what-you-need-to-know-about-javascript-number-type

This article will explain the following problems in detail:

  • 0.1 + 0.2 == 0.3 // false
  • 9007199254740992 == 9007199254740993 // true

Most static programming languages, such as Java and C, have many different numeric types.

For example, you can use Java byte or C char to store an integer [-128,127], both of which take only 1 byte. For larger integers, you can use either int or long, which take up 4 and 8 bytes respectively. For decimals, you can also use 4-byte floats or 8-byte doubles, which are often referred to as floating-point numbers. We’ll explain why.

However, JavaScript does not have such a wide variety of numeric types, and the ECMAScript standard defines only one digit type for the double-precision 64-bit binary format IEEE754. This type is used to store integers and decimals, and is almost identical to double in Java C. Developers new to JavaScript would expect 1 to be stored in memory like this:

However, the actual storage structure is:

This can cause a lot of confusion, so let’s look at the Java loop:

for (int i=1; 1/i > 0; i++) {
  System.out.println("Count is: " + i);
}
Copy the code

Think about it, how long will this program run?

It is not hard to see that the program will terminate after the first loop. On the second loop, the counter I will be increased to 2, and 1/2 evaluates to 0.5, but since I is of type INTEGER, it will be truncated to 0, so 1/2 > 0 will return false.

The same loop would look like this in JavaScript:

for (var i=1; 1/i > 0; i++) {
  console.log("Count is: " + i);
}
Copy the code

As a result, the above program never ends. Because 1/ I is a floating point number, not an integer.

Isn’t that interesting? Let’s move on.

Another particular case that developers unfamiliar with JavaScript mechanics often compare to other languages is 0.1+0.2=0.30000000000000004, which means that 0.1+0.2 does not equal 0.3. The related questions were searched so frequently that StackOverflow had to add a special reminder to the search box:

Interestingly, this problem is often labeled as JavaScript, but in fact it exists in any programming language that uses floating-point numbers to represent numbers. The same problem occurs when you use float or double in Java or C.

Another interesting point is that the result of 0.1 + 0.2 is not in the browser to print out the 0.30000000000000004, but 0.3000000000000000444089209850062616169452667236328125.

This article will explain how floating-point numbers work, as well as the for loop and 0.1+0.2 examples mentioned above.

It’s worth mentioning BigInt here, a new JavaScript base data type that represents arbitrarily large integers. With BigInt, you can safely store and manipulate even large integers that exceed Number’s safe integer limit. It’s available in this year’s V8 and is compatible with Chrome 67+ and Node V10.4.0 +. Click here to learn more.

To represent numbers in scientific notation

Before we get to floating point numbers and the IEEE754 standard, let’s look at how to represent a number using scientific notation:

Significant indicates the Significant part of a number, also called mantissa, precision. The zero is usually considered a placeholder, not a valid part.

Base represents the specific numeric system used, such as 10 for decimal and 2 for binary.

Exponent defines how many places the decimal point needs to move to the left or right to restore the original number.

Any number can be represented using scientific notation. For example, the number 2 can be represented in decimal and binary notation, respectively:

An exponent of 0 means no additional shift is required. Another example, 0.0000022 significant digit part is 22. Let’s move the decimal point to get rid of the zeros:

So this is how the decimal moves to the right and how the exponent changes. With this variation, we can make the raw number contain only significant digits:

By moving the decimal eight places to the right, we get the significant number 22. So we have to add a minus 8 to this exponent.

Similarly, in the following example, we get the significant number 22300000 by moving the decimal point to the left:

As you can see, scientific notation is very convenient for large and small numbers. With exponents, significant numbers can eventually be expressed as whole numbers or decimals. When converted to scientific notation, when the decimal point moves to the left, the exponent is positive; If we move the decimal to the right, the exponent is negative.

So what kind of number formats are standardized? A number normalized using scientific notation can only be preceded by a non-zero decimal point. Here is an example of formatting a number:

One thing you might also notice is that binary numbers always start with 1 before the decimal point. In this way, the numbers with standardized formats can be compared simply by comparing the mantissa in order.

We can think of scientific notation as a representation of floating point numbers, which indicate that the decimal point can float and can be placed anywhere in the significant digit. We know from the introduction above that the position of the decimal point depends on the exponent.

Floating point number based on IEEE754 standard

IEEE 754 specifies a number of floating-point algorithms, we will focus on numbers storage, carry, and addition operations. In another article, I explained in detail how binaries are rounded. Rounding is a common operation that occurs when a format does not have enough bits to store numbers. It’s important to understand how this works. Now let’s look at how numbers are stored, and we’ll use binary numbers as an example in all of the following examples.

How are numbers stored

Two common formats are defined in IEEE754 — single and double. They differ in the number of bits they use, and therefore in the range of numbers they can store. Similarly, the methods for converting numbers to these two formats are basically similar, with the only difference being that they assign different bits to significant bits and exponents.

IEEE754 floating point numbers are made up of sign bits, significant bits, and exponents. The following figure shows how the double format used by JavaScript Number types allocates these bits:

The sign bits occupy 1 bit, the exponent 11 bits, and the other 52 bits are allocated to mantissa (significant bits). The following table shows the bit allocation for each format:

Indices are stored in complement format. In another article that delves into complement format, I explain the difference between this and the other two implementations in detail. Please take a moment to understand this, as we will use it a lot in later transformations.

Integer storage

We talked about the allocation of bits above. Next, let’s look at how integers 1 and 3 are stored. The number 1 is represented as 1 in all base systems, so no additional conversion is required. It is represented in scientific notation:

With a mantissa of 1 and an exponent of 0, we might expect its floating-point representation to look something like this:

Is that the case? Unfortunately, JavaScript doesn’t provide a built-in function that visually shows what each bit is when a number is stored. To do this, I’ve written a simple function that lets us see how numbers are stored:

function to64bitFloat(number) {
    var i, result = "";
    var dv = new DataView(new ArrayBuffer(8));

    dv.setFloat64(0, number, false);

    for (i = 0; i < 8; i++) {
        var bits = dv.getUint8(i).toString(2);
        if (bits.length < 8) {
            bits = new Array(8 - bits.length).fill('0').join("") + bits;
        }
        result += bits;
    }
    return result;
}
Copy the code

Using the above method, we can see that the number 1 is stored like this:

This is completely different from what we thought. The mantissa is all zeros, and the exponent has a bunch of ones. Here, let’s find out.

First of all, we need to know that every number is converted to scientific notation, so what’s the advantage of doing that? If the number in front of the decimal point is always 1, we don’t have to allocate 1 bit of space to it, and the hardware will automatically fill in the 1 when we do the math. Since the number 1 has no digits after the decimal point in the standard format, and the number 1 before the decimal point does not need to be stored, its significant bits are all zeros.

And then, let’s see where all the ones in the exponent come from. As mentioned earlier, exponents are stored in complement format, so let’s calculate the offset:

As you can see, this is consistent with what we showed above, so according to the rules of complement, the value that is actually saved is 0, so if you have any doubts about this, you can read the binary complement.

Now let’s use the information we learned above to try to convert the number 3 to floating-point format. The binary of 3 is 11. If you don’t remember why, check out this binary to decimal conversion algorithm. Normally, the binary format for the number 3 would look like this:

There is only one digit 1 after the decimal point, which will be stored as the mantissa. Also, according to the previous introduction, the number 1 before the decimal point will not be stored. In addition, the exponent bit is 1. Let’s see how the binary complement is calculated:

Also, note that the mantissa parts are stored in the same order as in scientific notation — from left to right. With this concept, we can know the representation of the whole floating point number:

If you use the function I provided above, you get a consistent representation of floating-point numbers.

Why doesn’t 0.1+0.2 equal 0.3

Now that we know how numbers are stored, let’s take a look at this oft-cited example, which is simply explained as follows:

Only if the denominator of a decimal is an exponential of 2 can it be fully represented in binary format. And neither the denominator of 0.1 nor the denominator of 0.2 is an exponential multiple of 2, so they cannot be fully represented in binary format. Stored under the IEEE-754 floating-point standard, the significant bits in their mantras are rounded up to the maximum number of digits they can hold — 10 for half precision, 23 for single precision, and 52 for doubles. Due to the different bits used for different precision, floating point approximations of 0.1 and 0.2 May be slightly greater or less than the decimal representation. But it’s not equal. Therefore, 0.1+0.2==0.3 cannot be true.

The above explanation is probably clear enough for developers, but the best way to do it is to demonstrate the entire computational flow of the computer yourself, which is what we’ll do next.

Floating-point representations of 0.1 and 0.2

Let’s first look at the floating-point representation of 0.1. The first step is to convert 0.1 to binary by multiplying by 2. For the specific principle, please refer to my article on the decimal and binary conversion algorithm. After the conversion, we have an infinite repeating decimal:

Next, show it as standard scientific notation:

Since the mantissa can only have 52 digits at most, we need to carry the 52 decimal places.

Using the rounding rule defined in the IEEE754 standard, and the method described in my other article on rounding binary numbers, we get the rounded number:

Finally, calculate the complement of the index:

Then, we get a floating-point representation of the number 0.1:

It is recommended that you try to calculate the floating-point representation of 0.2 yourself, and eventually you will get the scientific notation and binary representation:

Calculate the result of 0.1+0.2

First, by converting 0.1 and 0.2 into the format of scientific notation, we get:

The addition operation requires that the numbers have the same exponent, and the rule requires that the numbers with smaller exponents be unified into larger exponents, so we convert the exponent of the first number from -4 to -3 to match the second number:

Next, add:

The result is now in floating-point format, so we need to standardize it, including rounding on demand, and calculating the complement in the exponent.

The normalized number triggers rounding, so we get:

Finally, floating point numbers are represented as:

This is the result of 0.1+0.2. To get this result, the computer has to round three times — twice for single numbers and once for addition. When the number 0.3 is stored separately, the computer rounds it only once. It is this difference that leads to the different binary representations of 0.1+0.2 and 0.3. When JavaScript executes 0.1+0.2 === 0.3, it is actually comparing these bit representations, and because they are different, it returns false. On the other hand, 0.1+0.2 === 0.3 is judged to be true even if 0.1 and 0.2 cannot be finitely represented in binary if the two bits are arranged in the same way.

Try verifying the 0.3 bit arrangement with the tool method to64bitFloat(0.3) I provided earlier, and you’ll see that the result is different from the 0.1+0.2 calculation we did above.

If you want to know the decimal value of the result, simply express the bits as scientific notation with an exponent of 0, and then convert it to decimal. Eventually you will get 0.1 + 0.2 actually stored in decimal number is 0.3000000000000000444089209850062616169452667236328125, While the decimal number 0.3 to 0.299999999999999988897769753748434595763683319091796875.

The answer to the infinite loop problem

To understand the infinite loop problem, there is a key number 9007199254740991, let’s talk about this particular number.

Number.MAX_SAFE_INTEGER

Entering number.max_safe_INTEGER on the console will print out our key Number 9007199254740991. Why is it so special that it even has its own constant name? Here’s what the ECMAScript Language Specification says about it:

MAX_SAFE_INTEGER indicates the largest safe integer n, so n and n+1 are actually the same Number. The value of number. MAX_SAFE_INTEGER is 9007199254740991 (2⁵³−1).

MDN also has some caveats:

By secure storage we mean being able to accurately distinguish between two different values, such as number.max_safe_INTEGER + 1 === number. MAX_SAFE_INTEGER + 2 will yield true, which is mathematically incorrect.

It’s important to note that this is not the largest number that JavaScript can represent. For example, the number 9007199254740994 represented by MAX_SAFE_INTEGER + 3 can be safely represented. With the constant number. MAX_VALUE, you get the maximum Number you can represent: 1.7976931348623157e+308. Surprisingly, some numbers between MAX_SAFE_INTEGER and MAX_VALUE are not represented correctly. In fact, MAX_SAFE_INTEGER and 9007199254740993 for MAX_SAFE_INTEGER+ 3 are one of them. If you type it into the console, you get 9007199254740992. It looks like JavaScript doesn’t take the original value, but instead subtracts the value after 1.

To find out, let’s look at the floating point representation of 9007199254740991 (MAX_SAFE_INTEGER) :

After conversion to scientific notation:

Now, to get the exponent to zero, we move the decimal 52 places to the right:

Now, we have used all the mantissa bits to store MAX_SAFE_INTEGER, and the exponent is 52. To store larger numbers, we have to add the exponent plus 1, which is 53, so we move the decimal 53 places to the right, and since we only have 52 mantras, we add zeros at the end. In the case of an exponent of 54, two zeros are added at the end, and in the case of 55, three zeros are added, and so on.

What effect would that have? You might have guessed it. Since all numbers greater than MAX_SAFE_INTEGER end with 0, any odd number greater than MAX_SAFE_INTEGER cannot be represented in the 64-bit floating-point standard. To store these numbers, the mantissa requires more than 52 bits of space. Let’s look at specific behaviors:

As you can see, 9007199254740993 and 9007199254740995 cannot be represented as 64-bit floating-point numbers, and the range of numbers that cannot be stored increases dramatically as the number increases.

An infinite loop

Let’s go back to the for loop:

for (var i=1; 1/i > 0; i++) {
    console.log("Count is: " + i);
}
Copy the code

The code above goes into an infinite loop. As I mentioned at the beginning of this article, this is because the result of 1/ I in JavaScript is not an integer, but a floating-point number. Now that you know how floating-point numbers work, and what number.max_safe_INTEGER means, it should make it easier to understand why it goes into an infinite loop.

The loop above stops if I reaches Inifinity, because 1/Infinity is false, but that doesn’t happen. In the previous section I explained why some integers cannot be stored and are rounded to the nearest even number. In this case, the counter I accumulates up to 9007199254740993, which is MAX_SAFE_INTEGER+2. This is the first integer that cannot be stored because it will be rounded to the nearest even number 9007199254740992. So the loop would get stuck on this number, causing an infinite loop here.

Talk a little bit about NaN and Infinity

Before I end this article, I want to briefly explain NaN and Infinity. Although both are considered special cases of floating-point numbers and floating-point operations, NaN stands for Not a Number, as opposed to Infinity. In addition, the index bit for both of them is 1024 (11111111111), while the index bit for number. MAX_VALUE is 1023 (111111111101).

Since NaN is also essentially a floating point Number, running Typeof NaN in a browser returns Number with all of its exponent bits 1 and only one mantissa that is not 0:

There are some mathematical operations that might yield NaN, such as 0/0 or math.sqrt (-4). There are also methods in JavaScript that may return NaN, such as parseInt(“s”) when the parseInt argument is a string. Interestingly, NaN always returns false when compared to any object. For example, the following operations return false:

NaN= = =NaN
NaN > NaN
NaN < NaN

NaN > 3
NaN < 3
NaN= = =3
Copy the code

Moreover, NaN is the only value that is not equal to itself. In addition, JavaScript provides the isNaN() method to detect whether a value isNaN.

Infinity is another special floating-point number that is used to handle overflows as well as mathematical operations such as 1/0. All the exponent bits of Infinity are ones and all the mantissa bits are zeros:

The sign bit for positive infinity is 0, and the sign bit for negative infinity is 1. MDN also describes some scenarios for returning to Infinity. Also, unlike NaN, Infinity can be used for safe comparisons.

Here-is-what-you-need-to-know-about-javascript-number-type