Why do we have 0.0 and -0.0 in Ruby?

Mathematical operations have real-number results, but we map those real results onto the nearest floating-point number, which is called "rounding". For every floating-point number, there is a range of real numbers that will round to that float, and sometimes it's useful to think of the float as being identified with that range of real numbers.

Since there is a finite supply of floating-point numbers, there must be a smallest positive float, and its opposite, the smallest (magnitude) negative float. But what happens to real number results even smaller than those? Well, they must "round to zero". But "a really small number greater than zero" and "a really small number less than zero" are pretty different things with pretty different mathematical behavior, so why should we lose the distinction between them, just because we're rounding? We don't have to.

So, the float 0 doesn't just include the real number 0, it also includes too-small-to-represent positive quantities. And the float -0 includes too-small-to-represent negative quantities. When you use them in arithmetic, they follow rules like "negative times positive equals negative; negative times negative equals positive". Even though we've forgotten almost everything about these numbers in the rounding process, we still haven't forgotten their sign.


You can assign a negative sign to a 0.0 float in Ruby because all IEEE 754 floating point numbers have a sign bit to indicate whether the number is positive or negative.

Here are the binary representations of 2.5 and -2.5:

[2.5].pack('f').unpack1('b*')
#=> "00000000000000000000010000000010"

[-2.5].pack('f').unpack1('b*')
#=> "00000000000000000000010000000011"

The last bit is the sign bit. Note that all the other bits are identical.

On the other hand, there is zero with a sign bit of 0:

['00000000000000000000000000000000'].pack('b*').unpack1('f')
#=> 0.0

and zero with a sign bit of 1:

['00000000000000000000000000000001'].pack('b*').unpack1('f')
#=> -0.0

Although 0.0 and -0.0 are numerically equal, they are not identical on the object level:

(0.0).eql?(-0.0)   #=> true
(0.0).equal?(-0.0) #=> false

Negative zeros have some special properties. For instance:

1 / 0.0    #=> Infinity
1 / -0.0   #=> -Infinity

Assigning - explicitly is not the only way to get -0.0. You may also get -0.0 as the result of a basic arithmetic operation:

-1.0 * 0 #=> -0.0