FizzBuzz in JavaScript with infinite arrays and multimapping
Published on in JavaScript
Fun way to solve the simple puzzle.
Infinitely repeating arrays
and mapping over multiple arrays
almost makes the remainder/modulo operator (%
) unnecessary.
Table of contents
TL;DR
Jump to the Finished result + playground section →
Rules of FizzBuzz
A FizzBuzz sequence consists of numbers and strings where:
- every 3rd item is "Fizz"
- every 5th item is "Buzz"
- every 15th item is "FizzBuzz"
- and other items are numbers.
So the first 15 items are:
- 1
- 2
- Fizz
- 4
- Buzz
- Fizz
- 7
- 8
- Fizz
- Buzz
- 11
- Fizz
- 13
- 14
- FizzBuzz
After that, the cycle repeats. (The 16th item is 16 instead of 1, and so on.)
The challenge
Here's a typical solution, though without a for
loop:
const result = Array.from({ length: 15 }, (_, i) => {
const n = i + 1
if (n % 15 === 0) return 'FizzBuzz'
if (n % 3 === 0) return 'Fizz'
if (n % 5 === 0) return 'Buzz'
return n
})
console.log(result)
// [
// 1, 2, 'Fizz', 4, 'Buzz',
// 'Fizz', 7, 8, 'Fizz', 'Buzz',
// 11, 'Fizz', 13, 14, 'FizzBuzz',
// ]
It works a-okay, but let's solve FizzBuzz without using the remainder operator in the usual way, i.e. without these:
if (n % 15 === 0)
if (n % 3 === 0)
if (n % 5 === 0)
Before that, we need to acquire two new skills: creating infinite arrays and mapping over multiple arrays.
Infinite arrays
How arrays normally work (d'oh):
const array = ['A', 'B', 'C']
array.length //=> 3
array[0] //=> 'A'
array[1] //=> 'B'
array[2] //=> 'C'
array[3] //=> undefined
array[4] //=> undefined
// ...
What we want:
when accessing an out-of-bounds numeric property of the array
object,
cycle back to the beginning of the array.
That can be achieved by creating
a Proxy
:
const proxyHandler = {
get(target, prop) {
// Accessing `target.length`
if (prop === 'length') {
return Infinity
}
// Accessing a numeric property of `target`, e.g. `target[5]`
if (typeof prop === 'string' && /^\d+$/.test(prop)) {
return target[prop % target.length]
}
// Accessing whatever else, e.g. `target.foo`
return target[prop]
},
}
const cycle = (array) => new Proxy(array, proxyHandler)
I named the function cycle
because I was inspired by
Clojure's cycle
function.
Usage:
const infiniteArray = cycle(['A', 'B', 'C'])
infiniteArray.length //=> Infinity
infiniteArray[0] //=> 'A'
infiniteArray[1] //=> 'B'
infiniteArray[2] //=> 'C'
infiniteArray[3] //=> 'A'
infiniteArray[4] //=> 'B'
infiniteArray[5] //=> 'C'
infiniteArray[6] //=> 'A'
// ...
Notes about the typeof prop
check
Why is the typeof prop === 'string'
check in the proxy handler's get
method needed?
Otherwise the regex test would throw an error
if prop
was a Symbol.
For example,
if prop
was
Symbol.iterator
:
;/^\d+$/.test(Symbol.iterator)
//=> TypeError: Cannot convert a Symbol value to a string
The error is thrown
because RegExp.prototype.test
expects a string argument,
and Symbols can't be implicitly converted to strings.
Symbol.iterator
is used by for...of
loops.
So,
using infiniteArray
in a for...of
loop
would thus throw an error
without the typeof
check.
Using any other Symbol property key would throw as well
without the typeof
check.
Why check that typeof prop
is 'string'
instead of 'number'
?
E.g. in infiniteArray[5]
the property key is a number, right?
Well yes, but actually no.
Numeric property keys are converted to strings. From Working with objects on MDN:
All keys in the square bracket notation are converted to string unless they're Symbols, since JavaScript object property names (keys) can only be strings or Symbols.
An alternative to the typeof prop
check
I said above that Symbols can't be implicitly converted to strings.
But a Symbol can be converted explicitly to a string by calling its toString()
method.
Doing so would make the typeof prop
check unnecessary:
-if (typeof prop === 'string' && /^\d+$/.test(prop)) {
+if (/^\d+$/.test(prop.toString())) {
return target[prop % target.length]
}
Neither option is very clear without an explanation.
I might prefer the toString()
alternative.
Shortcomings
The cycle
function is imperfect.
For example,
slicing doesn't work:
const infiniteArray = cycle(['A', 'B', 'C'])
// Current behavior:
infiniteArray.slice(0, 7)
//=> ['A', 'B', 'C', empty × 4]
// Ideal behavior:
infiniteArray.slice(0, 7)
//=> ['A', 'B', 'C', 'A', 'B', 'C', 'A']
Supporting all array methods would be interesting. I'll leave it to you as an exercise.
The current imperfect implementation is enough for solving FizzBuzz, so let's proceed.
Multimapping
Clojure's map
function
is similar to
JavaScript arrays' map
method,
except in Clojure you can map over multiple arrays at once:
map
returns a lazy sequence consisting of the result of applying [the function]f
to the set of first items of eachcoll
, followed by applyingf
to the set of second items in eachcoll
, until any one of thecoll
s is exhausted. Any remaining items in othercoll
s are ignored.
A multimap
function can be implemented in a few lines of JavaScript
(I'm ignoring the part about returning a lazy sequence):
const multimap = (fn, ...arrays) => {
const arrayLengths = arrays.map((array) => array.length)
const smallestArrayLength = Math.min(...arrayLengths)
return Array.from({ length: smallestArrayLength }).map((_, i) => {
const items = arrays.map((array) => array[i])
return fn(items, i)
})
}
Usage:
// Same lenghts -> loop over 4 times
multimap(
(args, i) => {
console.log(i, args)
return i
},
['A', 'B', 'C', 'D'],
['W', 'X', 'Y', 'Z']
)
// Console loggings:
// 0, ['A', 'W']
// 1, ['B', 'X']
// 2, ['C', 'Y']
// 3, ['D', 'Z']
// Return value:
// [0, 1, 2, 3]
// Different lengths -> loop over 3 times (the length of the shortest array)
multimap(
(args, i) => {
console.log(i, args)
return args[i]
},
['♠', '♡', '♢', '♣'],
['A', 'B', 'C', 'D', 'E'],
['♪', '♫', '☻']
)
// Console loggings:
// 0, ['♠', 'A', '♪']
// 1, ['♡', 'B', '♫']
// 2, ['♢', 'C', '☻']
// Return value:
// ['♠', 'B', '☻']
Putting the pieces together: solving FizzBuzz
With our two new skills – creating (imperfect) infinite arrays and mapping over multiple arrays – we are ready to tackle the fearsomely challenging puzzle of FizzBuzz.
Step 1: rules of FizzBuzz as arrays
Let's translate the rules of FizzBuzz to arrays (if that makes sense). Let's also focus only on the first ten items of the FizzBuzz sequence for now.
-
"Every 3rd item is 'Fizz'" means that every 3rd item of an array is
'Fizz'
. Other items are irrelevant, so they can be nulls:[null, null, 'Fizz', null, null, 'Fizz', null, null, 'Fizz', null]
-
"Every 5th item is 'Buzz'" works similarly:
[null, null, null, null, 'Buzz', null, null, null, null, 'Buzz']
-
"Every 15th item is 'FizzBuzz'" can be ignored for now because the rule doesn't apply to a sequence of just ten items. But it would work similarly to the two previous rules/arrays.
-
"Other items are numbers" is simply an array of numbers starting from 1:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Let's see how they look when put into the multimap
function:
multimap(
(args) => {
console.log(args)
},
[null, null, 'Fizz', null, null, 'Fizz', null, null, 'Fizz', null],
[null, null, null, null, 'Buzz', null, null, null, null, 'Buzz'],
[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
)
// Console loggings:
// [null, null, 1]
// [null, null, 2]
// ['Fizz', null, 3]
// [null, null, 4]
// [null, 'Buzz', 5]
// ['Fizz', null, 6]
// [null, null, 7]
// [null, null, 8]
// ['Fizz', null, 9]
// [null, 'Buzz', 10]
// Return value:
// [undefined × 10]
Hmm, what if we take the first non-null item from each set of items?
multimap(
(args) => args.find((x) => x !== null),
[null, null, 'Fizz', null, null, 'Fizz', null, null, 'Fizz', null],
[null, null, null, null, 'Buzz', null, null, null, null, 'Buzz'],
[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
)
//=> [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz']
Hey, FizzBuzz! 😎 (Every 15th item will be handled soon.)
Step 2: truthy vs falsy
The arrays contain only strings, numbers and nulls. Strings and numbers are truthy values, and nulls are falsy values.
Instead of taking the first non-null item, the first truthy item can be taken, and the result will be the same. This simplifies the callback function:
multimap(
(args) => args.find(Boolean),
[null, null, 'Fizz', null, null, 'Fizz', null, null, 'Fizz', null],
[null, null, null, null, 'Buzz', null, null, null, null, 'Buzz'],
[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
)
//=> [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz']
Step 3: cycling 🚴♂️
The first two arrays are repeating,
so they can be simplified with the new cycle
function:
multimap(
(args) => args.find(Boolean),
cycle([null, null, 'Fizz']),
cycle([null, null, null, null, 'Buzz']),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
)
//=> [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz']
Step 4: range
function
The third array is lame
because it's a manually-created range of numbers.
Let's create a range
function:
const range = (from, to) =>
Array.from({ length: to - from + 1 }, (_, i) => i + from)
range(5, 8)
//=> [5, 6, 7, 8]
Usage:
multimap(
(args) => args.find(Boolean),
cycle([null, null, 'Fizz']),
cycle([null, null, null, null, 'Buzz']),
range(1, 10)
)
//=> [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz']
Step 5: index
argument and filler arrays
The range
function is not actually even needed.
The callback function passed to the multimap
function
accepts index
as its second argument.
Just like the callback function passed to
Array.prototype.map
.
By using the index
argument,
the third array (range of numbers) is not needed,
and thus the range
function is not needed.
But the third array can't just be omitted as all of the remaining arrays would be infinite:
multimap(
(args, i) => args.find(Boolean) || i + 1,
cycle([null, null, 'Fizz']),
cycle([null, null, null, null, 'Buzz'])
)
//=> RangeError: Invalid array length
Remember that the multimap
function continues going over the arrays
until any of them is exhausted.
In other words,
multimap
goes over the arrays n times
where n is the length of the shortest array.
Thus, multimap
can be provided a "filler" array with a certain length.
Because the other arrays are infinite,
the filler array's length determines how many times multimap
goes over the arrays.
We are not interested in the filler array's values, so an array with ten empty slots is fine:
multimap(
(args, i) => args.find(Boolean) || i + 1,
cycle([null, null, 'Fizz']),
cycle([null, null, null, null, 'Buzz']),
Array(10) // [empty × 10]
)
//=> [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz']
The range
function might be clearer,
but I'm going to continue using the filler array,
because filler arrays will be useful in the next steps.
Step 6: left-padded arrays
The first two arrays are mostly fillers too. A more concise way of creating them is to create filler arrays and concatenating them with the last non-filler value:
// Before:
;[null, null, 'Fizz']
;[null, null, null, null, 'Buzz']
// After:
Array(2).concat('Fizz') // [empty × 2, 'Fizz']
Array(4).concat('Buzz') // [empty × 4, 'Buzz']
Kinda like left-padding arrays...
Someone quickly publish left-pad-array
on npm!
Usage:
multimap(
(args, i) => args.find(Boolean) || i + 1,
cycle(Array(2).concat('Fizz')),
cycle(Array(4).concat('Buzz')),
Array(10)
)
//=> [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz']
Step 7: the remaining rule
The code works so far with Fizzes and Buzzes, but not with FizzBuzzes. Every 15th item is "Fizz" instead of "FizzBuzz":
multimap(
(args, i) => args.find(Boolean) || i + 1,
cycle(Array(2).concat('Fizz')),
cycle(Array(4).concat('Buzz')),
Array(15)
)
//=> [1,
// 2,
// 'Fizz',
// 4,
// 'Buzz',
// 'Fizz',
// 7,
// 8,
// 'Fizz',
// 'Buzz',
// 11,
// 'Fizz',
// 13,
// 14,
// 'Fizz', // :( Should be 'FizzBuzz'
// ]
An easy fix is to use another array based on the remaining rule ("every 15th item is 'FizzBuzz'"):
multimap(
(args, i) => args.find(Boolean) || i + 1,
cycle(Array(14).concat('FizzBuzz')),
cycle(Array(2).concat('Fizz')),
cycle(Array(4).concat('Buzz')),
Array(15)
)
//=> [1,
// 2,
// 'Fizz',
// 4,
// 'Buzz',
// 'Fizz',
// 7,
// 8,
// 'Fizz',
// 'Buzz',
// 11,
// 'Fizz',
// 13,
// 14,
// 'FizzBuzz', // :) Correct
// ]
But the new array is not actually necessary because Fizz + Buzz = FizzBuzz:
multimap(
(args, i) => args.join('') || i + 1,
cycle(Array(2).concat('Fizz')),
cycle(Array(4).concat('Buzz')),
Array(15)
)
//=> [1,
// 2,
// 'Fizz',
// 4,
// 'Buzz',
// 'Fizz',
// 7,
// 8,
// 'Fizz',
// 'Buzz',
// 11,
// 'Fizz',
// 13,
// 14,
// 'FizzBuzz', // Still correct
// ]
Finished result + playground
const cycle = (array) =>
new Proxy(array, {
get(target, prop) {
if (prop === 'length') return Infinity
if (/^\d+$/.test(prop.toString())) return target[prop % target.length]
return target[prop]
},
})
const multimap = (fn, ...arrays) => {
const arrayLengths = arrays.map((array) => array.length)
const smallestArrayLength = Math.min(...arrayLengths)
return Array.from({ length: smallestArrayLength }).map((_, i) => {
const items = arrays.map((array) => array[i])
return fn(items, i)
})
}
const range = (from, to) =>
Array.from({ length: to - from + 1 }, (_, i) => i + from)
// Variation 1:
multimap(
(args) => args.find(Boolean),
cycle(Array(14).concat('FizzBuzz')),
cycle(Array(2).concat('Fizz')),
cycle(Array(4).concat('Buzz')),
range(1, 15)
)
// Variation 2:
multimap(
(args, i) => args.join('') || i + 1,
cycle(Array(2).concat('Fizz')),
cycle(Array(4).concat('Buzz')),
Array(15)
)
There are a lot more variations to discover. For example, these two are nice:
multimap(
([fizz, buzz, number]) => [fizz, buzz].join('') || number,
cycle([null, null, 'Fizz']),
cycle([null, null, null, null, 'Buzz']),
range(1, 15)
)
multimap(
([word], i) => word || i + 1,
cycle([null, null, 'Fizz', null, 'Buzz', 'Fizz', null, null, 'Fizz', 'Buzz', null, 'Fizz', null, null, 'FizzBuzz']),
Array(15)
)
Play around with the code on flems.io
What kind of cool variations can you come up with? Send me an email! 😎 (Address on the home page.)
Acknowledgements
The approach and code in this blog post were like 90% inspired by the YouTube video FizzBuzz in Clojure with & without modulus/remainder/rest by Fred Overflow (cool name!).
I wanted to call this blog post
"FizzBuzz in JS without the remainder operator,"
but I couldn't
as I had to use the operator in the cycle
function.
¯\(ツ)/¯