First, we observe that the FizzBuzz pattern is cyclic with a length of 15, since n % 3 = (n + 15) % 3
, and n % 5 = (n + 15) % 5
.
Whether n is divisible by 3 and/or 5 can be stored with two bits of information: 00 for neither, 01 for divisble by 3, 10 for divisible by 5, and 11 for divisible by both 3 and 5.
The FizzBuzz "answers" for the numbers 1 to 15 are as follows, from right to left:
11 00 00 01 00 10 01 00 00 01 10 00 01 00 00.
Notice that every third bit pair has the right bit set, and every fifth bit pair has the left bit set. The rightmost pair corresponds to the number 1, and the leftmost pair corresponds to the number 15. It is perhaps more clear to separate out the left bits and the right bits:
v5: 1. 0. 0. 0. 0. 1. 0. 0. 0. 0. 1. 0. 0. 0. 0.
v3: .1 .0 .0 .1 .0 .0 .1 .0 .0 .1 .0 .0 .1 .0 .0
v5|v3: 11 00 00 01 00 10 01 00 00 01 10 00 01 00 00
If we convert this string of bits to hexadecimal, we get the magic constant v from your snippet: 0x30490610
.
We can extract the bottom two bits of v
with the expression j = v & 3
, since the number 3 has the bottom two bits set and the rest unset. (This is the "bitwise AND"-operator of Python.)
We can cycle around the 2*15 = 30 bits by shifting v
two bits to the right v >> 2
, and then adding the two bits in the other end, (v >> 2) | (j << 28)
. (These are the left shift and right shift operators of Python, which also work in a bitwise fashion.)
In this way, v
can be seen as a "queue" containing 2-bit elements, each element corresponding to the "correct FizzBuzz answer" to one of the next 15 numbers to be processed. Once an element j
is popped from this queue, it is pushed onto the other end, so it is ready again in 15 iterations from now.
One final thing: The syntax print(m[j] if j else i)
means "If j
is not a falsy value such as 0, then print m[j]
; otherwise, print i
." Since m[1]
, m[2]
and m[3]
contain the right strings corresponding to our 2-bit representation of FizzBuzz answers, and j
is always in the range 0 to 3, the output is correct.
As an exercise, try to change v
to 0x39999999
and see if you can explain the behavior. (Hint: 9 in hexadecimal is 10 01 in binary.)
Update: Here's a variant of the program. I have replaced the hexadecimal value v
by an explicit queue q
of responses, and the scary-looking v = v >> 2 | j << 28
has been replaced by a pop from front and push to back, q.append(q.pop(0))
.
q = ['', '', 'Fizz', '', 'Buzz',
'Fizz', '', '', 'Fizz', 'Buzz',
'', 'Fizz', '', '', 'FizzBuzz']
for i in range(1, 101):
print(q[0] or i)
q.append(q.pop(0))
We can also add a separate fizz
and buzz
queue:
f = ['', '', 'Fizz']
b = ['', '', '', '', 'Buzz']
for i in range(1, 101):
print((f[0] + b[0]) or i)
f.append(f.pop(0))
b.append(b.pop(0))
Since ''
is a falsy value, (f[0] + b[0]) or i
will print the integer i
whenever both f[0]
and b[0]
is the empty string.