Programs, values, and types · Lesson 3 of 77 · concept
Numbers and arithmetic
About 50 minutes.
By the end of this lesson you can
- Use the six arithmetic operators and predict their results.
- Explain why 0.1 + 0.2 does not give exactly 0.3, and work around it.
- Tell a number apart from a piece of text that looks like a number.
- Recognise NaN and Infinity and say what produced them.
There is only one kind of number here
Explanation
Many languages make you choose: whole numbers go in one kind of box, numbers with a decimal point go in another, and big numbers go in a third. JavaScript does not. Every ordinary number — 3, -0.5, 1000000 — is stored in exactly the same format, a 64-bit double-precision floating-point value defined by a standard called IEEE 754.
That is a genuine simplification for you: 3 and 3.0 are the same value, and dividing two whole numbers can give you a fraction without any ceremony. It also has one consequence that surprises everybody, which the rest of this lesson is mostly about.
There is a second numeric type for very large whole numbers, called BigInt, written with an n on the end. You will not need it in this course and it is mentioned only so the word is not a mystery when you meet it.
The six arithmetic operators
Explanation
Addition is +, subtraction is -, multiplication is * and division is /. Two more are less familiar. The remainder operator % gives what is left over after a division: 7 % 3 is 1, because three goes into seven twice with one left over. The exponentiation operator ** raises to a power: 2 ** 10 is 1024.
Remainder is the workhorse of anything cyclical — every second item, every third row, the hours on a clock — because a remainder can only ever be one of a small fixed set of answers. Note that it takes its sign from the left-hand value, so -7 % 3 is -1, not 2. That is why it is called remainder and not modulo; the two behave differently for negative numbers, and JavaScript has the first one.
Order of operations, and the brackets that end the argument
Explanation
JavaScript follows the arithmetic precedence you were taught at school: ** first, then * / %, then + -, and operators of equal rank are worked out left to right. So 2 + 3 * 4 is 14, not 20.
Round brackets override all of it, and they cost nothing. Professional code is full of brackets that a precedence table says are unnecessary, because a reader should not have to consult a table. If you had to think about it for more than a second, add the brackets.
Why 0.1 + 0.2 is not 0.3
A mental model
Write one third as a decimal and you get 0.3333... forever. Stop anywhere and you have written down something slightly wrong. That is not a flaw in you or in decimals; it is that some fractions cannot be written exactly in base ten with a finite number of digits.
Computers store numbers in base two, and in base two it is 0.1 that cannot be written exactly. So the value stored when you write 0.1 is very slightly off, the same for 0.2, and when you add the two the small errors survive into the answer. 0.1 + 0.2 gives 0.30000000000000004.
Every mainstream language does this — Python, Java, C#, Excel — because they all use the same IEEE 754 format. It is not a JavaScript quirk, it is arithmetic on a finite machine.
Two practical rules follow. First, never compare two calculated fractional numbers with ===; compare whether the difference between them is smaller than a tolerance you choose. Second, for money, do not store 19.99 at all — store 1999 whole pennies, do all your arithmetic in whole numbers, and divide by 100 only when you display it.
toFixed hands you text, not a number
A common mistake
The usual way to show a number to two decimal places is toFixed(2), and it is the right tool for that job. But read its result carefully: (2.5).toFixed(2) produces the TEXT 2.50, not the number 2.5.
That matters because text and numbers behave differently the moment you do anything else with them — a subject with a whole lesson of its own at the end of this module. Use toFixed at the very last moment, when the value is on its way to a screen, and never in the middle of a calculation.
When arithmetic has no answer: NaN and Infinity
Explanation
Ask for something impossible and JavaScript does not stop the program. It produces a special value instead and carries on. Dividing by zero gives Infinity. Asking for a number from something that is not one — 0 divided by 0, or the square root of -1 — gives NaN, short for Not a Number.
NaN has a property no other value has: it is not equal to itself. NaN === NaN is false. That is deliberate, and it means the way to test for it is Number.isNaN(value), never value === NaN, which is false for absolutely everything.
NaN is contagious. Any arithmetic involving it produces NaN, so a single bad value early on can turn a whole page of results into nonsense with no error message anywhere. When a calculation gives you NaN, the bug is upstream of where you noticed it.
One quirk of this player: it prints values by converting them the way JSON does, and JSON cannot write NaN or Infinity, so printing them bare would show null. The example below wraps them in String(...) to see their real names. That is a limitation of this window onto the program, not of the values themselves.
Recap
Recap
All ordinary numbers share one floating-point format, so fractions are approximate: compare with a tolerance and keep money in whole pennies. % is remainder and takes the sign of its left operand; ** is a power. toFixed returns text. Impossible arithmetic yields Infinity or NaN, and NaN is detected with Number.isNaN because it is not equal to itself.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
All six operators on the same two numbers
Watch the same pair of numbers go through every arithmetic operator. Note that division gives a fraction without being asked, and that % is the leftover.
1console.log(7 + 3);2console.log(7 - 3);3console.log(7 * 3);4console.log(7 / 3);5console.log(7 % 3);6console.log(2 ** 10);- line 4
No whole-number division here: 7 / 3 gives a fraction, printed to as many digits as the format can distinguish.
- line 5
Three goes into seven twice, leaving 1. That leftover is what % gives you.
- line 6
2 ** 10 is two multiplied by itself ten times: 1024.
What it prints
104212.333333333333333511024
Precedence, and the brackets that settle it
The same digits in the same order give different answers depending on what is worked out first.
1console.log(2 + 3 * 4);2console.log((2 + 3) * 4);3console.log(10 - 4 - 3);- line 1
Multiplication outranks addition, so this is 2 + 12.
- line 2
Brackets win outright: 5 * 4.
- line 3
Equal rank, so left to right: (10 - 4) - 3, which is 3. Right to left would have given 9.
What it prints
14203
The 0.1 + 0.2 problem, and two ways past it
See the error with your own eyes, then see the two things professionals actually do about it: compare within a tolerance, or work in whole units.
1console.log(0.1 + 0.2);2console.log(0.1 + 0.2 === 0.3);3console.log(Math.abs(0.1 + 0.2 - 0.3) < 0.000001);4console.log((0.1 + 0.2).toFixed(2));5console.log(19.99 + 5.02);6console.log(Math.round(19.99 * 100) + Math.round(5.02 * 100));- line 1
The stored answer really is this. Nothing is being hidden from you.
- line 2
Which is why an exact comparison says false — correctly, if unhelpfully.
- line 3
The usual fix: is the gap between them smaller than an amount I am willing to ignore? Math.abs removes the sign so the direction of the error does not matter.
- line 4
toFixed(2) rounds for display and hands back TEXT. Fine on the way to a screen, wrong in the middle of a sum.
- line 5
Real money, added the obvious way. The answer is a hundred-millionth of a penny short of 25.01, and a receipt that prints it raw looks broken.
- line 6
The money approach: turn each price into whole pennies and add those instead. 2501 pennies is exactly 2501, with no error left to accumulate.
What it prints
0.30000000000000004falsetrue0.3025.0099999999999982501
Meeting NaN without being fooled by it
NaN is the value you get when a calculation has no numeric answer. Its strangest property — never equal to itself — is exactly what makes Number.isNaN necessary.
1const notANumber = 0 / 0;2console.log(String(notANumber));3console.log(Number.isNaN(notANumber));4console.log(notANumber === notANumber);5console.log(notANumber + 10 === notANumber + 10);6console.log(String(1 / 0));- line 1
Zero divided by zero has no sensible numeric answer, so the answer is NaN.
- line 2
String(...) is here only because this player prints through JSON, which has no way to write NaN. Without it you would see null.
- line 3
The correct test, and the only reliable one.
- line 4
The famous oddity: NaN is not equal to itself, so comparing a value to NaN can never be true.
- line 5
Contagion: arithmetic on NaN keeps producing NaN, and it stays unequal to itself.
- line 6
Dividing by zero is not an error in JavaScript; it produces Infinity.
What it prints
NaNtruefalsefalseInfinity
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
Two lines of arithmetic. What does each print?
console.log(7 % 3);console.log(7 / 2);
What kind of value does (2.5).toFixed(2) produce?
A calculation may have produced NaN. Which test tells you reliably?
Write it yourself
Exercise · core · 5 tests
Convert a temperature
The constant celsius holds 21.5. Work out the same temperature in Fahrenheit by multiplying by 9, dividing by 5 and adding 32. Declare three constants: fahrenheit (the number), roundedFahrenheit (that number rounded to the nearest whole number with Math.round) and label (the number written to one decimal place with toFixed(1)).
fahrenheit is a number close to 70.7, roundedFahrenheit is the whole number 71, and label is the text 70.7. fahrenheit is checked against a tolerance rather than exactly, because the arithmetic goes through a fraction that cannot be stored exactly.
Define fahrenheit, roundedFahrenheit and label at the top level of your code — the tests call them by name.
Exercise · stretch · 4 tests
Add up money without the error
Two prices are given as 19.99 and 5.02. Adding them directly gives 25.009999999999998 rather than 25.01. Declare totalCents, the total in whole pennies worked out by rounding each price times 100 and adding the results, then declare total, that number of pennies divided by 100.
totalCents is the whole number 2501 and total is exactly 25.01. The conversion to pennies must use Math.round, because neither 19.99 * 100 nor 5.02 * 100 lands on a whole number exactly.
Define totalCents and total at the top level of your code — the tests call them by name.
Worth remembering
- Why is 0.1 + 0.2 === 0.3 false, and what do you do instead?
Base-two floating point cannot store 0.1 or 0.2 exactly, so the sum is 0.30000000000000004. Compare the size of the difference against a tolerance, or work in whole units such as pennies.
- How do you test whether a value is NaN?
Number.isNaN(value). Comparing with === never works, because NaN is the only value that is not equal to itself.
- What does toFixed return?
Text, not a number. Use it at the point of display, never in the middle of a calculation.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
Check there: The description: JavaScript numbers are double-precision 64-bit IEEE 754 values; and the definitions of Number.isNaN and toFixed (which returns a string).
MDN — Expressions and operators
Check there: The arithmetic operators table, including remainder (%) and exponentiation (**).
Check there: That Math.round, Math.abs, Math.floor and Math.ceil are static methods on Math.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown