JavaScript rounds in a tricky way. It tricked all the engines, and even itself.
Math.round()
behaves the same as C’s familiar round
with one key difference: it rounds halfways (“is biased”) towards positive infinity. Here is its spec in ES 5.1. It suggests an implementation too:
The value of Math.round(x) is the same as the value of Math.floor(x+0.5)...
Unfortunately, the implementation in the spec does not correctly implement the spec.
One bug is that adding 0.5 can result in precision loss, so that a value less than .5 may round up to 1. Mac user? Try it yourself (Safari 11.0.3): One engine attempted to patch it by just checking for .5: However this fails on the other end: when x is large enough that fractional values can no longer be represented, x + 0.5 rounds up to x + 1, so JSRounding a large integer like Math.pow(2, 52) would actually increment it.
What's a correct implementation? SpiderMonkey checks on the high end, and exploits the loss of precision on the low end: fish's attempt just checks high and low: which produces surprisingly pleasant assembly, due to the compiler's fabs() and copysign() intrinsics.
Update: Steve Canon finds a floor-based approach, which surely must be optimal or close to it:
The ES6 spec sheepishly no longer suggests an implementation, it just disavows one:
Math.round(x) may also differ from the value of Math.floor(x+0.5) because of internal rounding when computing x+0.5...
JavaScript presumably rounds this odd way to match Java, and so the only engine to get it right out of the gate is Rhino, which simply calls back to Java's Math.round. Amusingly Oracle fell into the same trap with Rhino's successor Nashorn. Round and round we go!