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: