1

For reference, this code corresponds this codecademy exercise.
It's a simple cart/checkout program that registers an item and adds its cost to the total, it also takes a item quantity value.

Note: I had previously read about javascript being weird about math, so I was not shoked, but I'm very curious and interested in understanding what's the behavior in this particular case so maybe I'll get some insight to be able to prevent this sort of situation in the future.

So, I have 2 solutions to the exercise, the first one is OK-ish- and the second one has no problems, I guess. Here they are:

1rst one
Independently from the fact that this first approach is inefficient (going through all the posible cases multiple times and adding the item cost over and over). What caught my eye were the extra decimal values that got returned.

var cashRegister = {
    total:0,
    add: function(itemCost){
        this.total += itemCost;
    },
    scan: function(item,quantity) {
        for (var i = 1 ; i <= quantity ; i++ ){
            switch (item) {
            case "eggs": this.add(0.98); break;
            case "milk": this.add(1.23); break;
            case "magazine": this.add(4.99); break;
            case "chocolate": this.add(0.45); break;
            }
        }
    }
};

// scan each item 4 times
cashRegister.scan("eggs",4);
console.log('Your bill is '+ cashRegister.total);
cashRegister.scan("milk",4);
console.log('Your bill is '+ cashRegister.total);
cashRegister.scan("magazine",4);
console.log('Your bill is '+ cashRegister.total);
cashRegister.scan("chocolate",4);
console.log('Your bill is '+ cashRegister.total);

Console output:

Your bill is 3.92
Your bill is 8.840000000000002
Your bill is 28.800000000000004
Your bill is 30.6
Your bill is 30.6

2nd one
This solution multiplies the item quantity to any that happens to be the case.

var cashRegister = {
    total:0,
    add: function(itemCost){
        this.total += itemCost;
    },
    scan: function(item,quantity) {
        switch (item) {
        case "eggs": this.add(0.98*quantity); break;
        case "milk": this.add(1.23*quantity); break;
        case "magazine": this.add(4.99*quantity); break;
        case "chocolate": this.add(0.45*quantity); break;
        }
    }
};

// scan each item 4 times
cashRegister.scan("eggs",4);
console.log('Your bill is '+cashRegister.total);
cashRegister.scan("milk",4);
console.log('Your bill is '+cashRegister.total);
cashRegister.scan("magazine",4);
console.log('Your bill is '+cashRegister.total);
cashRegister.scan("chocolate",4);
console.log('Your bill is '+cashRegister.total);

Console output is clean:

Your bill is 3.92
Your bill is 8.84
Your bill is 28.8
Your bill is 30.6
Your bill is 30.6

So, what is going on with the first solution so that the decimal extravaganza gets returned?
Thank you very much in advance.

EDIT: Thank you for your attempts to help me with my doubts. Though I realized I need to make it clear that I'm aware "JavaScript uses floating point values for its number type and the loss of precision is the cause for this".

The real question is:
Why does the same calculation loses precision on the 1st solution but not the 2nd solution?

peem
  • 19
  • 4
  • First of all, why would you even want to do it the first way? Second, if you change the price of eggs to `0.01` the second way gives you a final total of `26.720000000000002`. Third, [this question](http://stackoverflow.com/questions/588004/is-floating-point-math-broken) may help you... – nnnnnn Jan 05 '15 at 09:08
  • Wanting to do it the first way or not, is irrelevant. But thank you for the link to that other question. – peem Jan 05 '15 at 09:17

2 Answers2

1

JavaScript uses floating point values for its number type, the loss of precision is the cause for this. The precision loss is due to the fact, that floating point numbers use binary encoded format (most likely IEEE 754), and it cannot represent most numbers precisely. E.g. 0.1 cannot be represented in IEEE 754:

  • Decimal: 0.1
  • Binary repr.: 00111101110011001100110011001101
  • Hexadecimal rept.: 0x3dcccccd,
  • Actual value: 0.10000000149011612

I have used this online converter for these result.

So to answer your question directly: more operations (multiple addition, instead of multiplication) on floating point numbers cause the accumulation of error.

An example

One of my favourite examples for precision loss, is this code:

var a = 0, b = 0;
for (var i = 1; i < 100; i++) {
    a += Math.pow(10, -9*i);
}
for (var j = 100; j < 1; j--) {
    b += Math.pow(10, -9*i);
}
alert(a == b);

This alerts false, because b is zero due to precision loss, while a is not.

Formatting

If you only care about the "nice" output, you may use the #toFixed method:

var num = 5.56789;
var n = num.toFixed(2); 

This yields "5.57".

The ECMAScript standard

According to the ECMAScript standard (ECMA-262, version 5.1), section 4.3.19. the definition of the Number value is the following:

primitive value corresponding to a double-precision 64-bit binary format IEEE 754 value

So, this is what a standards complaint implementation provides for you. Anything else, which may be in the background, is an implementation detail.

meskobalazs
  • 15,741
  • 2
  • 40
  • 63
  • I edited my question to better represent what I'm asking, thank you meskobalazs – peem Jan 05 '15 at 09:20
  • The example you added is very interesting, thank you for that, it seems you've really explored this issue, so I'm voting up, but it doesn't quite answer my question, can't accept it yet. // yikes, can't vote up because of low rep, sorry – peem Jan 05 '15 at 09:25
  • Thank you very much meskobalazs, your answer will be very helpful for anyone who lands on this question, that was enough to accept your answer and I asked a friend of mine to upvote you. Additionaly, I'm also guessing that the javascript VM works very differently while adding than while multiplying, do you have further detail on this? – peem Jan 05 '15 at 19:46
  • First of all, JavaScript is not run on a VM, but by JavaScript engines. I have updated my answer a bit, hope it is useful. – meskobalazs Jan 05 '15 at 19:59
  • Sorry, I meant V8, I'm using chrome. Thank you for the further detail. I can take it from here. – peem Jan 06 '15 at 02:33
0

This is due to the inherent imprecision in floating point numbers. Not all rational numbers can be represented in binary floating point as precise values, and so what you are seeing is the result of this approximation. This section on the Wikipedia page has a pretty good explanation of how this conversion is made.

If you really need precise values, you can use BigDecimal.js or other similar libraries.

Langston
  • 1,083
  • 10
  • 26
  • 1
    *"Not all rational numbers can be represented in floating point as precise values"* - And the problem is that JS uses *binary* floating point. (All of the numbers in the question can be represented precisely in *decimal* floating point.) – nnnnnn Jan 05 '15 at 09:16
  • I edited my question to better represent what I'm asking, thank you Langston – peem Jan 05 '15 at 09:20