Hello FizzBuzz, My Old Friend

A Swifty way of looking at that classic problem.

Hello FizzBuzz, My Old Friend

For those who don't know the FizzBuzz problem, a quick introduction: You want to print "Fizz" if the number's divisible by 3, "Buzz" if it's divisible by 5, "FizzBuzz" if it's divisible by both, otherwise print the number.

There are two common approaches to solving the classic FizzBuzz problem, as well as a classic argument over which is the better approach. Let's add a cool Swift language feature and see if we can improve on the existing solutions.

Appending Strings

My personal favorite has traditionally been the string appending method, appending each matched term in order:

for i in 1...110 {
	var string = ""
	
	if (i % 3) == 0 { // fish
		string.append("Fizz")
	}
	if (i % 5) == 0 { // fowl
		string.append("Buzz")
	}
	if string.count == 0 { // neither fish nor fowl
		string = "\(i)"
	}
	print(string)
}

The thing I like most about the above approach is that it's the one that requires the fewest changes if you want to add more cases. Need to test for 7, 11, and 13 too? Each requires a single condition. As someone who's worked on projects that have grown over time (with increasingly complex requirements), it feels more future-proof.

The traditional objection to this approach is that string handling, particularly things like string concatenation, aren't efficient. Back when we were working with machines that measured clock speeds in K and RAM in sippy cups, that was a genuine concern. But it feels like that judgement stuck even after it was no longer as relevant an objection as it used to be, particularly now that we're in the era of "JavaScript all the things" where [1, 2, 3] + [4, 5, 6] results in [1,2,34,5,6]. Really. (Because it treats the arrays as strings and contatenates them.)

The If Condition Approach

The more commonly used approace uses conditions:

for i in 1...110 {
	if (i % 3) == 0 && (i % 5) == 0 {
		print("FizzBuzz")
	} else if (i % 3) == 0 {
		print("Fizz")
	} else if (i % 5) == 0 {
		print("Buzz")
	} else {
		print("\(i)")
	}
}

While it's defensible enough (and is listed as the preferred method in a popular programming interview book), it starts getting fairly messy if you're testing for the 7, 11, and 13 cases. Currently, there are four cases (2! + 2). If you add a third term, it's eight cases (3! + 2). Five becomes unwieldy at (5! + 2) or 122.

Further, the extra cases are prone to data entry errors.

Swiftier Switches

There is a third way, using a switch statement, that's more idiomatic Swift, specifically:

for i in 1...110 {
	switch (i % 3, i % 5) {
	case (0, 0):	// divisible by both 3 and 5
		print("FizzBuzz")
	case (0, _):	// divisible by 3
		print("Fizz")
	case (_, 0):	// divisible by 5
		print("Buzz")
	default:
		print("\(i)")
	}
}

The short explanation: you're testing for modulo remainders on both 3 and 5 at the same time. Swift allows for more complex case statements than other languages, plus the wonderful _ which I call the DGAF case.

While this has most of the downsides of the second approach and doesn't reduce the number of cases, it's more visually obvious when there are entry errors, e.g., when you're also checking for 7:

for i in 1...110 {
	switch (i % 3, i % 5, i % 7) {
	case (0, 0, 0):
		print("FizzBuzzBeep")
	case (0, 0, _):
		print("FizzBuzz")
	case (0, _, 0):
		print("FizzBeep")
	case (_, 0, 0):
		print("BuzzBeep")
	case (0, _, _):	// divisible by 3
		print("Fizz")
	case (_, 0, _):	// divisible by 5
		print("Buzz")
	case (_, _, 0):	// divisible by 7
		print("Beep")
	default:
		print("\(i)")
	}
}

Combining Approaches

But let's not stop there.

You can simplify the number of cases by using the appending method with the Swift switch by using the fallthrough option, offering the best of both of the original approaches:

for i in 1...110 {
	var string = ""
	
	switch (i % 3, i % 5, i % 7) {
	case (0, _, _):	// divisible by 3
		string.append("Fizz")
		fallthrough
	case (_, 0, _):	// divisible by 5
		string.append("Buzz")
		fallthrough
	case (_, _, 0):	// divisible by 7
		string.append("Beep")
		// don't fallthrough to the default case
	default:
		string.append("\(i)")
	}
	print(string)
}

So with three match terms, we have four possible cases. With five terms, we'd have six cases, vastly superior to 122 cases in maintainability.

While FizzBuzz is a simple exercise that is sometimes used as an interview test equivalent to the "Can you fog up this mirror to prove you're alive?" level, there are still interesting things one can learn from it by playing with the language.

Photo by Alexander Grey on Unsplash

Tagged with: