JavaScript is a quite magical—and definitely wondrous—programming language, when you look at the Abstract Equality Comparison Algorithm, type coercion (yes, I’m looking at you !+[]+[]+![]
), and prototype pollution (both the dangerous and the ugly ways). 🧙♀️
Sooo, let’s say you want to round a number in JavaScript, specifically floor it. What do you do? It’s pretty straight forward.
Math.floor(number);
If we pick 25.96
for our number
, the code above will return 25
. But Math.floor(number)
is long, especially when you chain it with other functions. As someone who likes to use language features like the PHP spaceship operator to solve problems.
Therefore, let’s dive into other alternatives JavaScript has up it’s sleeve for flooring decimals. First though, I want to spoil you by saying that ~~number
is the shortest way to floor a number in JavaScript.
Why does ~~number
even work? And how so? Should we use it? Let’s explore! 🧭
String conversion
In JavaScript we can use Number#toString
to convert our number to a string. Strings can be split
by the decimal point. And then we can access the first array entry, and we get our 25.
+number.toString().split(".")[0];
Now this is quite a creative way of using string conversion. And it’s even longer than Math.floor(number)
. It has even more disadvantages, most notably, converting the number to a string. That is in and of itself an expensive operation.
Parsing it as an int
JavaScript wouldn’t be JavaScript with little global functions that only a few people really understand. parseInt
is one of them. It works a bit like (int)
casting in Java and C++.
parseInt(number);
Now this is good, and shorter than Math.floor
. However, it is intended for strings and cannot be used in functional methods like filter
, map
, and reduce
. Moreover, as detailed in this Stack Overflow answer, parseInt
also accepts “trailing characters that don't correspond with any digit”.
Type Coercion to the rescue!
Now we’re getting to the fun stuff. JavaScript is not strictly typed. You can round, for example, a string or an object and the function will return NaN
, “Not-a-Number”. But that also means that binary operators, such as binary AND (a & b
) and XOR (a ^ b
), can be used with types that are not a number, including the NaN
constant.
There is a catch with using floating point numbers, so non-integers, for binary operations. It will either break your numbers or... coerce them to integers?
Yes, that’s exactly what the double-tilde “operator” does. ~~number
for our example 25.95
will evaluate to 25
. The reason why it does is interesting. JavaScript will execute ToInt32
(number)
internally, leading to the number being converted to a 32-bit signed integer. In fact, all the following operators will convert number
to an Int32
in their calculations.
~number
: binary NOT, returns-26
number << 0
: left shift, returns25
number >> 0
: right shift, returns25
And the following operators will convert A @ B
—with @
being the operator and A
and B
being numbers—likewise to 32-bit signed integers. See the specification.
number ^ 0
: binary XOR, returns25
number & number
: binary AND, returns25
number | 0
: binary OR, returns25
The Double Tilde
As we’ve just learned, ~number
will convert our number to a 32-bit signed integer. Using ~~number
will reverse the binary NOT operation and leave us with 25
. Great! 🎉
You might want to point out concerns with maintainability. And you are absolutely right, let’s hold off on that though. There is a technical reason why you should never use the double-tilde to floor a number. It fails the overflow-test.
Let’s construct a reasonable example.
You have a business banking system which allows customers to create transactions. You know that using decimals is bad for currencies, so you use integers that count the amount of money in cent. Now, you want to floor the transaction amount to avoid other problems. That is why you use the double-tilde to round the incoming amount.
One day, a customer wants to transfer 4284967296
cent ($42,849,672.96) from their account to an account at another bank. You do your checks and then floor the amount with the double-tilde operator. ~~4284967296
. Surprisingly, the balance of the customer’s account just increased by $10,000.
Wait... What? 🤔
The conversion to a 32-bit signed integer with ToInt32(number)
not only involves the floor
ing but also a calculation of modulo 232 on the floored number. I'm not sure why this is done but my educated guess is that it is to avoid overflowing the 32-bit integer internally.
4284967296 % (2 ** 32) => -1000000
And now you have to explain your boss how using two tilde characters in your bank’s software calculation caused a customer to get $10,000 from you for free. If they had just read the documentation.
Maintainability
After learning that the answer to “Should we use it?” is a straightforward HELL NO, let’s just quickly consider the maintainability aspect of using an undocumented language feature.
If JavaScript had a overflow-safe floor operator, using it in a professional environment would sound good to me. One could argue that it wouldn't be good because Math.floor(number)
expresses a clearer intent. And yes, probably if you have never used the operator before.
Using an undocumented hack that nobody understands—in the presence of a proper solution!—will make your code less understandable.
Conclusion
A few years ago, I got a double-tilde flooring through a code review but the reviewer asked me to comment what it does it because I argued it was more readable. Oh boy, was I wrong. As far as I know, this code is still out in the wild, although I might’ve changed it to a proper Math.floor
at some point... Luckily, I’ve never gotten the chance to implement this at a bank before finding out about it’s danger.
I leave you with this: don’t ever use the double-tilde “operator” for flooring a number. It’s bad at best and dangerous at worse. 🙅♀️