When working in a high-level language like Ruby, it’s quite rare seeing bitwise operators used. Nevertheless, knowing how they work might still be very helpful at times.
So what does an operator being bitwise actually mean? It means that instead of treating integers as whole numbers, it treats them as a sequence of bits.
Because of this, being able to inspect the binary representations of integers
makes understanding how bitwise operators work much easier. To convert an
integer to a string of ones and zeros, use the Fixnum#to_s
method, passing
2
as the argument:
13.to_s(2) #=> "1101"
Ruby has 6 bitwise operators:
operator | description |
---|---|
& |
bitwise and |
| |
bitwise or |
^ |
bitwise exclusive or |
~ |
bitwise not (complement) |
<< |
bitwise left shift |
>> |
bitwise right shift |
Bellow follows explanations of each of them.
The bitwise and operator
The bitwise and operator walks through the binary representation of
two integers bit by bit. If the bits at the same position in both integers are
1
the resulting integer will have the corresponding bit set to 1
. If not,
the bit will be set to 0
.
(a = 18).to_s(2) #=> "10010"
(b = 20).to_s(2) #=> "10100"
(a & b).to_s(2) #=> "10000"
The bitwise or operator
The bitwise or operator works the same as the bitwise and operator
with the exception that it will set the bit in the resulting integer to 1
as
long as at least one of the corresponding bits in the given integers is 1
:
(a = 18).to_s(2) #=> "10010"
(b = 20).to_s(2) #=> "10100"
(a | b).to_s(2) #=> "10110"
The bitwise exclusive or operator
The bitwise exclusive or operator will set the bit to 1
in the
output if only one of the corresponding bits in the inputs is set to 1
:
(a = 18).to_s(2) #=> "10010"
(b = 20).to_s(2) #=> "10100"
(a ^ b).to_s(2) #=> "110" (leading zeros omitted)
The bitwise not operator
The bitwise not, or complement, operator flips the bits inside an
integer turning zeros to ones and ones to zeros. This sounds simple but is a bit
harder to show and explain. First, lets see what Fixnum#to_s
can show us
about this:
(a = 18).to_s(2) #=> "10010"
~a #=> -19
(~a).to_s(2) #=> "-10011"
That doesn’t look very flipped to me! And what is that minus sign doing there?
It turns out that Fixnum#to_s
won’t return the underlying binary
representation of negative numbers. Instead it returns the binary representation
of the corresponding positive number prepended with a minus sign:
-19.to_s(2) #=> "-10011"
"-" + 19.to_s(2) #=> "-10011"
This is because in mathematics, negative numbers are denoted with a minus sign prefix regardless of their base. However, in computer hardware where everything is represented using ones and zeros this is not possible. Instead, other methods has to be used.
In modern computers, negative integers are stored in memory using a method called two’s complement. The two’s complement method is designed to make basic arithmetic operations simple to implement and can be summarized in three rules:
-
0
is represented by all zeros. -
Positive numbers starts at zero and counts upward towards a maximum value of 2(n-1)-1 where n is the number of bits. This means that the maximum value possible to represent using 4 bits is 2(4-1)-1 = 7 or
0111
. -
For negative numbers, the meaning of zeros and ones changes. Instead of starting at zero, negative numbers starts at
-1
, which is represented using all ones, and are counted downward using zeros towards a minimum of -2(n-1). In the case of a 4 bit number that would be -2(4-1) = -8 or1000
.
This means that instead of being able to represent the numbers 0 to 15, 4 bits can represent the numbers -8 to 7. Here are some examples of positive and negative integers and their binary representation:
binary | decimal |
---|---|
0000 |
0 |
0001 |
1 |
0010 |
2 |
0111 |
7 |
1111 |
-1 |
1110 |
-2 |
1101 |
-3 |
1000 |
-8 |
If Fixnum#to_s
can’t help us, how do we get hold of the underlying binary
representation of negative numbers?
To do this, we’ll have to turn to the Fixnum#[]
method. This
method returns the value of the bit at the given position in the integer with
0
being the rightmost. This means that we can loop through the interesting
positions in the integer and collect their bit values:
a = 18
b = ~a #=> -19
5.downto(0).map { |n| a[n] }.join #=> "010010"
5.downto(0).map { |n| b[n] }.join #=> "101101"
At last, we can see the effect of the bitwise not operator. Reading the rules
above, we can also understand why 101101
in this case means -19
rather than
45
The bitwise left and right shift operators
The bitwise left and right shift operators shifts an integer’s bits to the left or right by the given number of positions, truncating bits or padding with zeros where needed.
a = 18
(a >> 2).to_s(2) #=> "100"
(a >> 1).to_s(2) #=> "1001"
(a).to_s(2) #=> "10010"
(a << 1).to_s(2) #=> "100100"
(a << 2).to_s(2) #=> "1001000"
To learn more about how to use these operators, check out the followup post Flags, Bitmasks, and Unix File System Permissions in Ruby.