Introduction
Raindrops is a slightly more complex version of the FizzBuzz challenge, a classic interview question.
Instructions
Instructions
Your task is to convert a number into its corresponding raindrop sounds.
If a given number:
- is divisible by 3, add “Pling” to the result.
- is divisible by 5, add “Plang” to the result.
- is divisible by 7, add “Plong” to the result.
- is not divisible by 3, 5, or 7, the result should be the number as a string.
Examples
- 28 is divisible by 7, but not 3 or 5, so the result would be
"Plong".
- 30 is divisible by 3 and 5, but not 7, so the result would be
"PlingPlang".
- 34 is not divisible by 3, 5, or 7, so the result would be
"34".
A common way to test if one number is evenly divisible by another is to compare the [remainder][remainder] or [modulus][modulo] to zero.
Most languages provide operators or functions for one (or both) of these.
[remainder]: https://exercism.org/docs/programming/operators/remainder
[modulo]: https://en.wikipedia.org/wiki/Modulo_operation
Dig Deeper
if statements
if Statements
def convert(num):
sounds = ''
if num % 3 == 0: sounds += 'Pling'
if num % 5 == 0: sounds += 'Plang'
if num % 7 == 0: sounds += 'Plong'
return sounds or str(num)
This approach is the most straightforward or ‘naive’ - it replicates in code what the instructions say, using if statements to check the modulo for each factor.
If the number is evenly divisible by the factor (modulo == 0), the corresponding string is concatenated to sounds via the + operator.
Sounds is returned if it is not empty (see Truth Value Testing for more info).
Otherwise, a str version of the input number is returned.
This, of course incurs the ‘penalty’ of string concatenation.
But since there are only three factors to check and the strings are small, the concatenation is at a minimum.
In fact, this solution - and most others described in the approaches here - are O(1) time complexity.
There are a constant number of factors to iterate through, and the work that is done never increases, even as the input numbers get bigger.
This holds true for space complexity as well.
The compact form for the if statements might be harder to read for some people.
These can be re-written to be nested, and the return can be re-written to use a ternary expression:
def convert(num):
sounds = ''
if num % 3 == 0:
sounds += 'Pling'
if num % 5 == 0:
sounds += 'Plang'
if num % 7 == 0:
sounds += 'Plong'
return sounds if sounds else str(num)
While this solution is nicely readable and to-the-point, it will grow in length and get harder to read if many more factors are added or business logic changes.
Other solutions using data structures to hold factors might be a better option in ‘high change’ situations.
Loop and f-string
Sequence(s) with a Loop and f-string
def convert(number):
sounds = ''
drops = ("i", 3), ("a", 5), ("o", 7)
for vowel, factor in drops:
if number % factor == 0:
sounds += f'Pl{vowel}ng'
return sounds or str(number)
This approach loops through the drops tuple (although any iterable sequence(s) can be used), unpacking each vowel and factor.
If the input number is evenly divisible by the factor (modulus == 0), the corresponding vowel is inserted into the f-string for that factor.
The f-string is then concatenated to sounds string via +.
Sounds is returned if it is not empty.
Otherwise, a string version of the input number is returned.
This takes O(1) time and O(1) space.
It is a very efficient and clever way of building up the return string, since only one vowel is changing per ‘drop’.
However, it might take a moment for others reading the code to understand what exactly is going on.
It also (may) create maintenance difficulties should there be future factors and sounds that do not conform to the pattern of only changing the vowel in the sound.
A much less exciting (but perhaps easier to maintain) rewrite would be to store the whole drop sound and build up the return string out of whole drops:
def convert(number):
sounds = (3, 'Pling'), (5, 'Plang'), (7, 'Plong')
output = ''
for factor, sound in sounds:
if number % factor == 0:
output += sound
return output or str(number)
This has the same time and space complexity as the first variation.
Sequence(s) with str.join()
Sequence(s) with str.join()
def convert(number):
drops = ["Pling","Plang","Plong"]
factors = [3,5,7]
sounds = ''.join(drops[index] for
index, factor in
enumerate(factors) if (number % factor == 0))
return sounds or str(number)
This approach is very similar to the loop and f-string approach, but uses two lists to hold factors and sounds and enumerate() to extract an index.
It also converts the loop that calculates the remainders and sounds into a generator expression within join().
Sounds is returned if it is not empty.
Otherwise, a str version of the input number is returned.
This, like most approaches described here is O(1) time and space complexity.
In benchmark timings, the generator-expression and multiple variables add a bit of overhead.
Readability here might not be the easiest for those not used to reading generator expressions inside calls to other functions, so if that is the case, this can be re-written as:
def convert(number):
drops = ["Pling","Plang","Plong"]
factors = [3,5,7]
sounds = (drops[index] for index, factor in
enumerate(factors) if (number % factor == 0))
return ''.join(sounds) or str(number)
Source: Exercism python/raindrops