Monday, February 1, 2016

A List Oriented Approach To The Fizz Buzz Test

What is the FizzBuzz Test?


"Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”."
http://c2.com/cgi/wiki?FizzBuzzTest

Here is a classic solution.

for n in range(1, 101):
     line = ""
     if n%3 == 0:
         line = line + "Fizz"
     if n%5 == 0:
         line = line + "Buzz"

     if line:
         print line
     else:
         print n

Can we do better?

The for-loop solution gives the correct answer, but it's completely untestable. There's no way to check the intermediate results.

Here is a list-based solution that will allow us to check the results to any level of confidence we desire.

(NOTE: I'm only using the numbers 1..15 to keep the examples short.)

First, we generate 2 lists of words. 

fizz = ['Fizz' if i % 3 == 0 else '' for i in range(1,16)]
buzz = ['Buzz' if i % 5 == 0 else '' for i in range(1,16)]

You can check your work


list(enumerate(fizz,1))

[(1, ''),
 (2, ''),
 (3, 'Fizz'),
 (4, ''),
 (5, ''),
 (6, 'Fizz'),
 (7, ''),
 (8, ''),
 (9, 'Fizz'),
 (10, ''),
 (11, ''),
 (12, 'Fizz'),
 (13, ''),
 (14, ''),
 (15, 'Fizz')]


list(enumerate(buzz,1))
[(1, ''),
 (2, ''),
 (3, ''),
 (4, ''),
 (5, 'Buzz'),
 (6, ''),
 (7, ''),
 (8, ''),
 (9, ''),
 (10, 'Buzz'),
 (11, ''),
 (12, ''),
 (13, ''),
 (14, ''),
 (15, 'Buzz')]


Then we zip them and concatenate the strings to produce the output text.


fizzbuzz = [f+b for f,b in zip(fizz,buzz)]
['',
 '',
 'Fizz',
 '',
 'Buzz',
 'Fizz',
 '',
 '',
 'Fizz',
 'Buzz',
 '',
 'Fizz',
 '',
 '',
 'FizzBuzz']

Enumerating this list gets us the final answer.

list(enumerate(fizzbuzz,1))
[(1, ''),
 (2, ''),
 (3, 'Fizz'),
 (4, ''),
 (5, 'Buzz'),
 (6, 'Fizz'),
 (7, ''),
 (8, ''),
 (9, 'Fizz'),
 (10, 'Buzz'),
 (11, ''),
 (12, 'Fizz'),
 (13, ''),
 (14, ''),
 (15, 'FizzBuzz')]

Now we just have to decide whether to print the text or the number.

Putting it all together


# processing

fizz = ['Fizz' if i % 3 == 0 else '' for i in range(1,16)]
buzz = ['Buzz' if i % 5 == 0 else '' for i in range(1,16)]
fizzbuzz = [f+b for f,b in zip(fizz,buzz)]
solution  = [s if s else i for i,s in enumerate(fizzbuzz,1)]

# output

for x in solution:
    print x 


This solution is 4 lines shorter than the original for-loop solution, but I think there are two other advantages that are more important:

Advantage #1: clean separation between the calculations and output. It's easier to focus on the logic of solving the problem. The print statement is almost an afterthought.

Advantage #2: we've captured the intermediate calculations and we can check them. For example, what if you made this mistake in your logic (getting the modulo test backwards)

fizz = ['Fizz' if i % 3 else '' for i in range(1,16)]

It would be easy to see that the logic is backwards

[(1, 'Fizz'),
 (2, 'Fizz'),
 (3, ''),
 (4, 'Fizz'),
 (5, 'Fizz'),
 (6, ''),
 (7, 'Fizz'),
 (8, 'Fizz'),
 (9, ''),
 (10, 'Fizz'),
 (11, 'Fizz'),
 (12, ''),
 (13, 'Fizz'),
 (14, 'Fizz'),
 (15, '')]

That's a lot harder to see when the calculation is buried inside a for-loop.

A direct solution

There is a direct solution which is shorter and faster than anything else. It can be done with

  • no looping
  • no if tests
  • no use of the % operator

Start with a list of numbers.

fizzbuzz = range(16)

Replace every multiple of 3 with 'Fizz'.

fizzbuzz[3::3] = ['Fizz' for i in fizzbuzz[3::3]]

Replace every multiple of 5 with 'Buzz'.

fizzbuzz[5::5] = ['Buzz' for i in fizzbuzz[5::5]]

Replace every multiple of 15 with 'FizzBuzz'.

fizzbuzz[15::15] = ['FizzBuzz' for i in fizzbuzz[15::15]]

for i in fizzbuzz[1:]  :  print i


I have a lot more tips and tricks in my book Mastering Python Lists.

No comments:

Post a Comment