34

Problem

I was trying to build out a list of heights in the console by meteres starting from 1.20m and ending up at 2.50m.

I used this code:

var heights = [];
for ( var i=1.20, l=2.5; i<l; i+=0.01 ){

    heights.push(i);

}

heights = heights.join('\n');

If I console.log( heights ) I get:

1.2
1.21
1.22
1.23
...

But then at 1.37 I start getting:

1.37
1.3800000000000001
1.3900000000000001
1.4000000000000001
1.4100000000000001
1.4200000000000002
1.4300000000000002

Questions

  • What's going on?
  • How do I fix it?

Demo

var heights = [];
for ( var i=1.20, l=2.5; i<l; i+=0.01 ){

    heights.push(i);

}

var heights = heights.join('\n');

document.querySelector('#output').innerText = heights;
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>JS Bin</title>
</head>
<body>
  <div id="output"></div>
</body>
</html>
hitautodestruct
  • 20,081
  • 13
  • 69
  • 93

6 Answers6

18

You are doing this fine. The problem is with the inaccuracy of floating point numbers.

Why are floating point numbers so inaccurate?

If you wish to display this number then use:

heights[i].toFixed(2);

Note that toFixed() returns a string and you will have to convert back to a float (parseFloat()) if you want to perform more numerical operations.

George Reith
  • 13,132
  • 18
  • 79
  • 148
  • 7
    +1 for mentioning about `toFixed()` returns a string. – Mics Oct 10 '13 at 08:29
  • 2
    Since this is already the accepted answer, it should be considered that scaling numbers rather than truncating them might be a better choice, not just because it avoids converting numbers to strings and back to numbers. – Ingo Bürk Oct 10 '13 at 08:49
  • @IngoBürk That is a good point, personally if I was using numbers that required such a high level of precision I wouldn't be using decimals for anything but display purposes anyway. There are many numbers that can't be properly represented as decimal e.g. `1/3` and these will result in inaccuracies anyway no matter whether you scale them or not. – George Reith Oct 10 '13 at 12:33
9

This is just because of how math works in JavaScript, you can find lots of answers explaining about it - like this one. The easiest solution is to just do everything times 100, and then divide when adding to the array e.g.

var heights = [], i, l;
for (i = 120; i < 250; i += 1){    
    heights.push(i / 100);    
}

You could use toFixed but that will give you a String as the result.

Community
  • 1
  • 1
phenomnomnominal
  • 5,427
  • 1
  • 30
  • 48
  • No it's not because "That is just because of how math works in JavaScript", it's a behavior, which is caused by the way floating points are stored in the computer. – vallentin Oct 10 '13 at 08:32
  • Of course but saying "That is just because of how math works in JavaScript" the way you say it, is that it only occurs in JavaScript, because thats just how JavaScript's math works, which is wrong. As it isn't JavaScript's fault! – vallentin Oct 10 '13 at 08:34
  • 1
    That's your interpretation of it. It *is* because of how maths works in JavaScript, I never mentioned it being a special thing about JavaScript, and I linked to an answer giving a detailed explanation of the problem. The issue is well-known and has a great existing answer. What really matters here is the solution to this particular case. – phenomnomnominal Oct 10 '13 at 08:36
  • It's not because of how math works, it's not a mathematical error, the error occurs because of the way floating points get stored on the computer. – vallentin Oct 10 '13 at 08:40
  • 3
    I'll +1 this anyway because I like scaling the numbers better than `toFixed`. – Ingo Bürk Oct 10 '13 at 08:48
  • The way the characteristics of binary floating point affect programs is influenced by language design choices, such as how widely to use binary floating point and whether to offer a decimal floating point alternative. If the question had been about Java rather than Javascript, I might have suggested use of BigDecimal, as an alternative to manual scaling. It is about "how math works in Javascript". – Patricia Shanahan Oct 10 '13 at 09:42
2

This is due to how floating point numbers are stored internally, it's not JavaScript specific. You can use .toFixed() to store a string representation of the number with the desired accuracy, so:

heights.push(i.toFixed(2));

If you want to avoid storing strings and then converting them back into an actual number, you can multiply the step so that it becomes a whole number and then store a division instead:

for (var i = 120; i <= 250; ++i) {
    heights.push(i / 100);
}

The difference besides the fact that you now have numbers is that multiple of 0.1 are represented in single accuracy, e.g. 2.4 instead of "2.40".

Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
1

This is because machines use base 2, and you are using base 10 numbers that cannot be accurately represented in base 2 with a floating point number.

You can use this library to format it: https://github.com/dtrebbien/BigDecimal.js

Red Alert
  • 3,786
  • 2
  • 17
  • 24
1

As it has been mentioned, toFixed() returns String, and parseFloat() converts String to Float. parseFloat() also removes trailing zeros, which makes sense, but was not working for my use case.

Here is an example of iteration using Float and retaining trailing zeroes.

var i = 0.9,
  floats = [];

while (i < 2) {
  i = (i + 0.1).toFixed(1);
  floats.push(i);
  i = parseFloat(i);
}
console.log(floats);


[ '1.0',
  '1.1',
  '1.2',
  '1.3',
  '1.4',
  '1.5',
  '1.6',
  '1.7',
  '1.8',
  '1.9',
  '2.0' ]

If trailing zeroes are not needed, the loop can be simplified as:

while (i < 2) {
  floats.push(i);
  i = parseFloat((i + 0.1).toFixed(1));
}
Vadym Tyemirov
  • 8,288
  • 4
  • 42
  • 38
0

Use the method toFixed(float), to limit the amount of digit after comma

heights.push(i.toFixed(2));
mcamier
  • 426
  • 2
  • 15