Jun 26, 2019 11:47 · 744 words · 4 minute read
Do you know why the following code will end up charging $5 to the user? 😨
const discount = (amount, quantity) => amount - quantity; let amount = 1.5 amount = discount(amount,1.2) // 1.5 - 1.2 = 0.3 amount = discount(amount,0.3) // 0.3 - 0.3 = 0 return parseInt(amount) // 0
discount function subtracts the discount amount from the total amount we have to pay.
The user is lucky as we’ll discount the total amount he had to pay. However, somehow, we end up charging $5 to him. 🙈
This unexpected behavior comes from two different issues tied to the language.
The first one appears when we subtract
1.5 - 1.2, the expected result would be
0.3 but the real result is
Because of this small issue, when we subtract,
0.3 we wind up with
0.00000000000000004 instead of
0, and here is when the second issue appears.
What we would expect from
parseInt is to leave the value untouched, however, this is not happening.
Once you execute
parseInt(1.5-1.2-0.3)the result will be 5. 😓
Let’s delve in what’s happening here.
parseInt function is doing one thing under the hood. It transforms the number to its string representation before parsing it.
The funny thing here is that given 7 decimals or more the string representation changes from
1e-7 and, from here, everything gets messed up.
parseInt function was designed to handle not only decimals but octal and hexadecimal values too. In order to do that it returns the first number of the string instead of failing.
(1.5-1.2-0.3) it’s displayed as
5.551115123125783e-17. If we parse this to an integer it will consider only the first number of the string
5.551115123125783 and it will remove all decimals.
In that way we’ll end up with
5, which is exactly what we charged to our customer.
Let’s see another example:
const sum = (a,b) => a + b; const cutDecimals = (num) => num.toFixed(2) const sumItemPrices = itemPrice => parseFloat(itemPrice.map(cutDecimals).reduce(sum,0)); const itemPrices = [1,3,1.345] return sumItemPrices(itemPrices)
The expected output for this function should be
1+3+1.54 = 5.34 but instead the value returned it’s
This particular issue is locaten in the
toFixed funtion. This function is meant to be a decorative function returning a
string value instead of a
number (a bit counterintuitive don’t you think?).
Because of that when we
cutDecimals of our array of numbers
[1,3,1.345] it get’s transformed into an array of strings like this
["1","3","1.345"] therefore when we apply the sumation function to every single value instead of summing we are concatenating the values ending up with the following string
"01.003.001.34". If we try to
parseFloat this string it will stop on the second dot as will consider the rest as unvalid characters and it will return
Now let’s talk a bit about types. Let’s see this function:
const isNumber = num => typeof num === "number";
They look reasonable and correct but it’s not, in fact, have an edge case in which don’t behave as expected.
isNumber(1) // -> true, it's ok isNumber("x") // -> false, it's ok isNumber(parseInt("x")) // -> true, ???
What’s happening here is that
NaN (not a number) and the type of “not a number” is a number, which again can be kind of confusing.
And you can find more thinks in the language that can go against your common sense like these ones:
typeof null // -> "object" typeof  // -> "object" isNaN() // -> false undefined == null // -> true
For that reason we created a small open-source library called keyu which pretends to relieve developers from this hassle. For now it’s a very small and humble library but with your collaboration can be a good reference for the rest.
If you are interested to contribute you can find the source code here
If you feel the pain, contribute to it. If you like it, share it and if you don’t, please, criticize it.