Numbers

All numbers in JavaScript are stored in 64-bit format IEEE-754 also known as “double precision”.

Let’s recap what we know about them and add a little bit more.

More ways to write a number

Imagine, we need to write a billion. The obvious way is:

let billion = 1000000000;

But in real life we usually dislike writing many zeroes. It’s easy to mistype. Also we are lazy. We we usually write something like "1bn" for a billion or "7.3bn" for 7 billions 300 millions. The similar is true for other big numbers.

In JavaScript, we can do almost the same by appending the letter "e" to the number and specifying the zeroes count:

let billion = 1e9;  // 1 billion, literally: 1 and 9 zeroes

alert( 7.3e9 );  // 7.3 billions (7,300,000,000)

In other words, "e" multiplies the number by 1 with the given zeroes count.

1e3 = 1 * 1000
1.23e6 = 1.23 * 1000000

Now let’s write something very small. Say, 1 microsecond (one millionth of a second):

let ms = 0.000001;

Also the same "e" can help. If we’d like not to write down the zeroes explicitly, the same number is:

let ms = 1e-6; // six zeroes to the left from 1

If we count the zeroes in 0.000001, there are 6 of them. So naturally it’s 1e-6.

In other words, a negative number after "e" means a division by 1 with the given number of zeries:

// -3 divides by 1 with 3 zeroes
1e-3 = 1 / 1000 (=0.001)

// -6 divides by 1 with 6 zeroes
1.23e-6 = 1.23 / 1000000 (=0.00000123)

Hex, binary and octal numbers

Hexadecimal numbers are widely used in JavaScript: to represent colors, encode characters and for many other things. So there exists a short way to write them: 0x and then the number.

For instance:

alert( 0xff ); // 255
alert( 0xFF ); // 255 (the same, case doesn't matter)

Binary and octal numeral systems are rarely used, but also supported using 0b and 0o prefixes:

let a = 0b11111111; // binary form of 255
let b = 0o377; // octal form of 255

alert( a == b ); // true, the same number 255 at both sides

There are only 3 numeral systems with such support. For other numeral systems we should use function parseInt (later in this chapter).

toString(base)

The method num.toString(base) returns a string representation of num in the numeral system with the given base.

For example:

let num = 255;

alert( num.toString(16) );  // ff
alert( num.toString(2) );   // 11111111

The base can vary from 2 to 36. By default it’s 10.

Most often use cases are:

  • base=16 is used for hex colors, character encodings etc, digits can be 0..9 or A..F.

  • base=2 is mostly for debugging bitwise operations, digits can be 0 or 1.

  • base=36 is the maximum, digits can be 0..9 or A..Z. The whole latin alphabet is used to represent a number. A funny, but useful case for 36 is when we need to turn a long numeric identifier into something shorter, for example to make a short url. Can simply represent it in the numeral system with base 36:

    alert( 123456..toString(36) ); // 2n9c
Two dots to call a method

Please note that two dots in 123456..toString(36) is not a typo. If we want to call a method directly on a number, like toString in the example above, then we need to place two dots .. after it.

If we placed a single dot: 123456.toString(36), then there would be an error, because JavaScript syntax implies the decimal part after the first dot. And if we place one more dot, then JavaScript knows that the decimal part is empty and now goes the method.

Also could write (123456).toString(36).

Rounding

One of most often operations with numbers is the rounding.

There are following built-in functions for rounding:

Math.floor
Rounds down: 3.1 becomes 3, and -1.1 becomes -2.
Math.ceil
Rounds up: 3.1 becomes 4, and -1.1 becomes -1.
Math.round
Rounds to the nearest integer: 3.1 becomes 3, 3.6 becomes 4 and -1.1 becomes -1.
Math.trunc (not supported by Internet Explorer)
Removes the decimal part: 3.1 becomes 3, -1.1 becomes -1.

Here’s the table to summarize the differences between them:

Math.floor Math.ceil Math.round Math.trunc
3.1 3 4 3 3
3.6 3 4 4 3
-1.1 -2 -1 -1 -1
-1.6 -2 -1 -2 -1

These functions cover all possible ways to deal with the decimal part as a whole. But what if we’d like to round the number to n-th digit after the point?

For instance, we have 1.2345 and want to round it to 2 digits, getting only 1.23.

There are two ways to do so.

  1. Multiply-and-divide.

    For instance, to round the number to the 2nd digit after the point, we can multiply the number by 100, call the rounding function and then divide back.

    let num = 1.23456;
    
    alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
  2. The method toFixed(n) rounds the number to n digits after the point and returns a string representation of the result.

    let num = 12.34;
    alert( num.toFixed(1) ); // "12.3"

    The rounding goes to the nearest value, similar to Math.round:

    let num = 12.36;
    alert( num.toFixed(1) ); // "12.4"

    Please note that result of toFixed is a string. If the decimal part is shorter than required, zeroes are appended to its end:

    let num = 12.34;
    alert( num.toFixed(5) ); // "12.34000", added zeroes to make exactly 5 digits

    We can convert it to a number using the unary plus or a Number() call: +num.toFixed(5).

Imprecise calculations

Internally, a number is represented in 64-bit format IEEE-754. So, there are exactly 64 bits to store a number: 52 of them are used to store the digits, 11 of them store the position of the decimal point (they are zero for integer numbers) and 1 bit for the sign.

If a number is too big, it would overflow the 64-bit storage, potentially giving an infinity:

alert( 1e500 ); // Infinity

But what may be a little bit more obvious, but happens much often is the loss of precision.

Consider this (falsy!) test:

alert( 0.1 + 0.2 == 0.3 ); // false

Yes, indeed, if we check whether the sum of 0.1 and 0.2 is 0.3, we get false.

Strange! What is it then if not 0.3?

alert( 0.1 + 0.2 ); // 0.30000000000000004

Ouch! There are more consequences than an incorrect comparison here. Imagine you’re making an e-shopping site and the visitor puts $0.10 and $0.20 goods into his chart. The order total will be $0.30000000000000004. That would surprise anyone.

Why does it work like that?

A number is stored in memory in it’s binary form, as a sequence of ones and zeroes. But fractions like 0.1, 0.2 that look simple in the decimal numeric system are actually unending fractions in their binary form.

In other words, what is 0.1? It is one divided by ten 1/10, one-tenth. In decimal numeral system such numbers are easily representable. Compare it to one-third: 1/3. It becomes an endless fraction 0.33333(3).

So, division by powers 10 is guaranteed to look well in the decimal system, but the division by 3 is not. For the same reason, in the binary numeral system, the division by powers of 2 is guaranteed to look good, but 1/10 becomes an endless binary fraction.

There’s just no way to store exactly 0.1 or exactly 0.2 in the binary system, just like there is no way to store one-third as a decimal fraction.

The numeric format IEEE-754 solves that by storing the nearest possible number. There are rounding rules that normally don’t allow us to see that “tiny precision loss”, so the number shows up as 0.3. But the loss still exists.

We can see it like this:

alert( 0.1.toFixed(20) ); // 0.10000000000000000555

And when we sum two numbers, then their “precision losses” sum up.

That’s why 0.1 + 0.2 is not exactly 0.3.

Not only JavaScript

The same issue exists in many other programming languages.

PHP, Java, C, Perl, Ruby give exactly the same result, because they are based on the same numeric format.

Can we work around the problem? Sure, there’s a number of ways:

  1. We can round the result with the help of a method toFixed(n):

    let sum = 0.1 + 0.2;
    alert( sum.toFixed(2) ); // 0.30

    Please note that toFixed always returns a string. It ensures that it has 2 digits after the decimal point. That’s actually convenient if we have an e-shopping and need to show $0.30. For other cases we can use the unary plus to coerce it into a number:

    let sum = 0.1 + 0.2;
    alert( +sum.toFixed(2) ); // 0.3
  2. We can temporarily turn numbers into integers for the maths and then go back. That would looks like this:

    alert( (0.1*10 + 0.2*10) / 10 ); // 0.3

    It works, because when we get 0.1*10 = 1 and 0.2 * 10 = 2 then both numbers are integers, there’s no precision loss for them.

  3. If it’s a shop, then the most radical solution would be to store all prices in cents. No fractions at all. But what if we apply a discount of 30%? In practice, totally evading fractions is rarely feasible, so the solutions listed above are here to help.

The funny thing

Try running this:

// Hello! I'm a self-increasing number!
alert( 9999999999999999 ); // shows 10000000000000000

The reason is the same: loss of precision. There are 64 bits for the number, 52 of them can be used to store digits, and that’s not enough. So the least significant digits disappear.

JavaScript doesn’t trigger an error in such case. It does the best to fit the number into the format. Unfortunately, the format is not big enough.

Two zeroes

Another funny consequence of the internal representation is the existance of two zeroes: 0 and -0.

That’s because a sign is represented by a single bit, so every number can be positive or negative, including the zero.

In most cases the distinction is unnoticeable, because operators are suited to treat them as the same.

Tests: isFinite and isNaN

Remember the two special numeric values?

  • Infinite (and -Infinite) is a special numeric value that is greater (less) than anything.
  • NaN represends an error.

They belong to the type number, but are not “normal” numbers, so there are special functions to check for them:

  • isNaN(value) converts its argument to a number and then tests if for being NaN:

    alert( isNaN(NaN) ); // true
    alert( isNaN("str") ); // true

    But do we need the function? Can we just use the comparison === NaN? Sorry, but no. The value NaN is unique in that it does not equal anything including itself:

    alert( NaN === NaN ); // false
  • isFinite(value) converts its argument to a number and returns true if it’s a regular number, not NaN/Infinity/-Infinity:

    alert( isFinite("15") ); // true
    alert( isFinite("str") ); // false, because a special value: NaN
    alert( isFinite(Infinity) ); // false, because a special value: Infinity

Sometimes isFinite is used to validate the string value for being a regular number:

let num = +prompt("Enter a number", '');

// will be true unless you enter Infinity, -Infinity or not a number
alert( isFinite(num) );

Please note that an empty or a space-only string is treated as 0 in all numeric functions including isFinite.

Compare with Object.is

There is a special built-in method Object.is that compares values like ===, but is more reliable for two edge cases:

  1. It works with NaN: Object.is(NaN, NaN) === true, that’s a good thing.
  2. Values 0 and -0 are different: Object.is(0, -0) === false, it rarely matters, but these values technically are different.

In all other cases, Object.is(a, b) is the same as a === b.

This way of comparison is often used in JavaScript specification. When an internal algorithm needs to compare two values for being exactly the same, it uses Object.is (internally called SameValue).

parseInt and parseFloat

The numeric conversion using a plus + or Number() is strict. If a value is not exactly a number, it fails:

alert( +"100px" ); // NaN

The sole exception is spaces before and after the line, they are ignored.

But in real life we often have values in units, like "100px" or "12pt" in CSS. Also in many countries the currency symbol goes after the amount, so we have "19€" and would like to extract a numeric value out of that.

That’s what parseInt and parseFloat are for.

They “read” a number from a string until they can. In case of an error, the gathered number is returned. Function parseInt reads an integer number, parseFloat reads any number:

alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5

alert( parseInt('12.3') ); // 12, only integer part
alert( parseFloat('12.3.4') ); // 12.3, the second point stops the reading

Of course, there are situations when parseInt/parseFloat return NaN. It happens when no digits could be read:

alert( parseInt('a123') ); // NaN, the first symbol stops he process
The second argument of parseInt(str, radix)

The parseInt() function has an optional second parameter. It specifies the base of the numeral system, so parseInt can also parse strings of hex numbers, binary numbers and so on:

alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255, without 0x also works

alert( parseInt('2n9c', 36) ); // 123456

Other math functions

JavaScript has a built-in Math object which contains a small library of mathematical functions and constants.

A few examples:

Math.random()

Returns a random number from 0 to 1 (not including 1)

alert( Math.random() ); // 0.1234567894322
alert( Math.random() ); // 0.5435252343232
alert( Math.random() ); // ... (any random numbers)
Math.max(a, b, c...) / Math.min(a, b, c...)

Return the greatest/smallest from the arbitrary number of arguments.

alert( Math.max(3, 5, -10, 0, 1) ); // 5
alert( Math.min(1, 2 ); // 1
Math.pow(n, power)

Returns n raised the given power

alert( Math.pow(2, 10) ); // 2 in power 10 = 1024

There are more functions and constants in Math, including trigonometry, you can find them in the docs for the Math object.

Summary

To write big numbers:

  • Append "e" with the zeroes count to the number. Like: 123e6 is 123 with 6 zeroes.
  • A negative number after "e" causes the number to be divided by 1 with given zeroes. That’s for one-millionth or such.

For different numeral systems:

  • Can write numbers directly in hex (0x), octal (0o) and binary (0b) systems
  • parseInt(str, base) parses an integer from any numeral system with base: 2 ≤ base ≤ 36.
  • num.toString(base) converts a number to a string in the numeral system with the given base.

For converting values like 12pt and 100px to a number:

  • Use parseInt/parseFloat for the “soft” conversion, which reads a number from a string until it can.

For fractions:

  • Round using Math.floor, Math.ceil, Math.trunc, Math.round or num.toFixed(precision).
  • Remember about the loss of precision when working with fractions.

More mathematical functions:

  • See the Math object when you need them. The library is very small, but can cover basic needs.

Tasks

importance: 5

Create a script that prompts the visitor to enter two numbers and then shows their sum.

Run the demo

P.S. There is a gotcha with types.

let a = +prompt("The first number?", "");
let b = +prompt("The second number?", "");

alert( a + b );

Note the unary plus + before prompt. It immediately converts the value to a number.

Otherwise, a and b would be string their sum would be their concatenation, that is: "1" + "2" = "12".

importance: 4

According to the documentation Math.round and toFixed both round to the nearest number: 0..4 lead down while 5..9 lead up.

For instance:

alert( 1.35.toFixed(1) ); // 1.4

How do you think why in the similar example below 6.35 is rounded to 6.3, not 6.4?

alert( 6.35.toFixed(1) ); // 6.3

How to round 6.35 the right way?

Internally the decimal fraction 6.35 is an endless binary. As always in such cases, it is stored with a precision loss.

Let’s see:

alert( 6.35.toFixed(20) ); // 6.34999999999999964473

The precision loss can cause both increase and decrease of a number. In this particular case the number becomes a tiny bit less, that’s why it rounded down.

And what’s for 1.35?

alert( 1.35.toFixed(20) ); // 1.35000000000000008882

Here the precision loss made the number a little bit greater, so it rounded up.

How can we fix the problem with 6.35 if we want it to be rounded the right way?

We should use bring it closer to an integer prior to rounding:

alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000

Note that 63.5 has no precision loss at all. That’s because the decimal part 0.5 is actually 1/2. Fractions divided by powers of 2 are exactly represented in the binary system, now we can round it:

alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 63(rounded) -> 6.3
importance: 5

Create a function readNumber which prompts for a number until the visitor enters a valid numeric value.

The resulting value must be returned as a number.

The visitor can also stop the process by entering an empty line or pressing “CANCEL”. In that case, the function should return null.

Run the demo

Open a sandbox with tests.

function readNumber() {
  let num;

  do {
    num = prompt("Enter a number please?", 0);
  } while ( !isFinite(num) );

  if (num === null || num === '') return null;

  return +num;
}

alert(`Read: ${readNumber()}`);

The solution is a little bit more intricate that it could be because we need to handle null/empty lines.

So we actually accept the input until it is a “regular number”. Both null (cancel) and empty line also fit that condition, because in numeric form they are 0.

After we stopped, we need to treat null and empty line specially (return null), because converting them to a number would return 0.

Open the solution with tests in a sandbox.

importance: 4

This loop is infinite. It never ends. Why?

let i = 0;
while (i != 10) {
  i += 0.2;
}

That’s because i would never equal 10.

Run it to see the real values of i:

let i = 0;
while (i < 11) {
  i += 0.2;
  if (i > 9.8 && i < 10.2) alert( i );
}

None of them is exactly 10.

Such things happen because of the precision losses when adding fractions like 0.2.

Conclusion: evade equality checks when working with decimal fractions.

importance: 2

The built-in function Math.random() creates a random value from 0 to 1 (not including 1).

Write the function random(min, max) to generate a random floating-point number from min to max (not including max).

Examples of its work:

alert( random(1, 5) ); // 1.2345623452
alert( random(1, 5) ); // 3.7894332423
alert( random(1, 5) ); // 4.3435234525

You can use the solution of the previous task as the base.

We need to “map” all values from the interval 0…1 into values from min to max.

That can be done in two stages:

  1. If we multiply a random number from 0…1 by max-min, then it the interval of possible values increases 0..1 to 0..max-min.
  2. Now if we add min, the possible interval becomes from min to max.

The function:

function random(min, max) {
  return min + Math.random() * (max - min);
}

alert( random(1, 5) );
alert( random(1, 5) );
alert( random(1, 5) );
importance: 2

Create a function randomInteger(min, max) that generates a random integer number from min to max including both min and max as possible values.

Any number from the interval min..max must appear with the same probability.

Examples of its work:

alert( random(1, 5) ); // 1
alert( random(1, 5) ); // 3
alert( random(1, 5) ); // 5

The simple but wrong solution

The simplest, but wrong solution would be to generate a value from min to max and round it:

function randomInteger(min, max) {
  let rnd = min + Math.random() * (max - min);
  return Math.round(rnd);
}

alert( randomInteger(1, 3) );

The function works, but it is incorrect. The probability to get edge values min and max is two times less than any other.

If you run the example above many times, you would easily see that 2 appears the most often.

That happens because Math.round() gets random numbers from the interval 1..3 and rounds them as follows:

values from 1    ... to 1.4999999999  become 1
values from 1.5  ... to 2.4999999999  become 2
values from 2.5  ... to 2.9999999999  become 3

Now we can clearly see that 1 gets twice less values than 2. And the same with 3.

The correct solution

There are many correct solutions to the task. One of them is to adjust interval borders. To ensure the same intervals, we can generate values from 0.5 to 2.5, thus adding the required probabilities to the edges:

function randomInteger(min, max) {
  // now rnd is from  (min-0.5) to (max+0.5)
  let rnd = min - 0.5 + Math.random() * (max - min + 1);
  return Math.round(rnd);
}

alert( randomInteger(1, 3) );

An alternative way could be to use Math.floor for a random number from min to max+1:

function randomInteger(min, max) {
  // here rnd is from min to (max+1)
  let rnd = min + Math.random() * (max + 1 - min);
  return Math.floor(rand);
}

alert( randomInteger(1, 3) );

Now all intervals are mapped this way:

values from 1  ... to 1.9999999999  become 1
values from 2  ... to 2.9999999999  become 2
values from 3  ... to 3.9999999999  become 3

All intervals have the same length, making the final distribution uniform.

Tutorial map

Comments

read this before commenting…
  • You're welcome to post additions, questions to the articles and answers to them.
  • To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
  • If you can't understand something in the article – please elaborate.