codingpaws

Don't use the double-tilde in JavaScript

Jun 19, 2021 · 5 min read

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 stackoverflow answer, parseInt also accepts “trailing characters that don't correspond with any digit”.

Type Coercion to the resuce!

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.

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.

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 flooring 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. 🙅‍♀️