Javascript: FizzBuzzFactory

Solving katas can be fun. Sometimes, though, they can drive you insane.

Javascript: FizzBuzzFactory

Recently in my Slack I posted a stupid little JS FizzBuzz solution:

const range = (min, max) => {
  const arr = [];
  for (i = min ; i <= max ; i++) {
    arr.push(i);
  }
  return arr;
}

const maker = (num, word) => (x) => x % num === 0 ? word : "";
const resulter = (x) => {
  const res = maker(3, "fizz")(x) + maker(5, "buzz")(x);
  return res.length > 0 ? res : x;
}

range(1, 15).map(x => resulter(x)).forEach(x => console.log(x))

It's not anything brilliant, it's not even an ideal solution, but I thought it was interesting enough to throw up on Slack. A friend of mine pointed out a related CodeWars kata and encouraged me to run through it. The kata gives a fairly straight forward premise involving creating a function that creates functions that can perform fizzbuzz based on an arbitrary set of data. "Interesting," I said to myself, "I wonder if I can convert my existing FizzBuzz solution to fix this!" So off to work I went and quickly came up with a solution without checking any of the expected tests for the kata.

const maker = (num, word) => (x) => x % num === 0 ? word : "";

const FizzBuzzFactory = (x) => {
  const first = maker(x[0][0], x[0][1])
  const second = maker(x[1][0], x[1][1])
  const third = maker(x[2][0], x[2][1])
  
  return (num) => {
    let result = third(num);
    result = result.length > 0 ? result : second(num);
    result = result.length > 0 ? result : first(num);
    result = result.length > 0 ? result : num;
    return result;
  }
}

And thus a solution was born! To quickly go over the kata, imagine we're given an array of arrays that looks like the following: [[2, "Fizz"], [4, "Buzz"], [6, "FizzBuzz"]]. With this we want to create a factory function that takes in this array and returns a function that accepts a number and produces the accurate FizzBuzz response for the number. In the situation where more than one value match, always choose the response of the highest number. "Great! This was easy!" I thought, "Just need to put it into the test and. . ." Enter problem number one: The response always needs to be a String.

"Fine, no big deal." I thought to myself, making the requisite code change:

const maker = (num, word) => (x) => x % num === 0 ? word : "";

const FizzBuzzFactory = (x) => {
  const first = maker(x[0][0], x[0][1])
  const second = maker(x[1][0], x[1][1])
  const third = maker(x[2][0], x[2][1])
  
  return (num) => {
    let result = third(num);
    result = result.length > 0 ? result : second(num);
    result = result.length > 0 ? result : first(num);
    result = result.length > 0 ? result : '' + num;
    return result;
  }
}

This is when I hit the first major snag. As mentioned earlier, I hadn't bothered to even read the expected test cases, taking the wording of the problem at face value (A rookie mistake, by all accounts.) It turns out that the FizzBuzzFactory needs to account for a few additional cases I hadn't thought about: An empty array, and arrays of larger than 3 sizes. "Again, not a problem," I thought, coding furiously, "This is an easy fix, I've solved harder things in Java, let me just leverage that experience."

const maker = (num, word) => (x) => x % num === 0 ? word : "";

const FizzBuzzFactory = (x) => {
  if (x.length === 0) {
    return (num) => {return '' + num}
  }
  const funcArray = [];
  x.forEach(pair => {
    funcArray.push(maker(pair[0], pair[1]));
  });
  
  return (num) => {
    let result = '';
    funcArray.reverse().forEach(func => {
      result = result.length > 0 ? result : func(num);
    });
    result = result.length > 0 ? result : '' + num;
    return result;
  }
}

This time I even made sure to have some test cases:

const test = FizzBuzzFactory([[2,"Foo"],[4,"Bar"],[6,"FooBar"]]);

console.log(test(2))
console.log(test(3))
console.log(test(4))
console.log(test(10))
console.log(test(12))

Which resulted in a satifying

Foo
3
Bar
Foo
FooBar

"Perfect, I'm finally done!" I said once again, throwing my code into the test and smuggly hitting Attempt.

Error: Passed 4, expected 'Bar' actual value 'Foo'

"Crap baskets." This was infuriating. I had a test of 4, it always came back with Bar! "Ah ha! Something must be wrong with their test! If I run this locally it works fine, even if I do so in a console window in Chrome and Firefox." I thought to myself, like the damned idiot I am. Just to prove my point, I changed my own test slightly to mimic what their test was doing:

console.log(test(2))
console.log(test(4))

Which, much to my surprise, yield an error:

Foo
Foo

"How??? This shouldn't be possible, yet here we are!" I screamed, brandishing my mighty Java Axe against the horde of non-sensical Javascript invaders. "What could be causing it? I take in the array, I reverse it, and I run the forEach, this should be fi-" It was at about this point the obvious answer hit me like a bag of bricks thrown off a skyscraper. Reverse performs an in-place reversal of arrays in Javascript and doesn't create a new array in reverse order. I had forgotten this, in most other languages I work in the equivalent method creates a new instance so you don't end up manipulating the original array. Every time the method was run it would reverse the existing array so the next time it was called it would be in the wrong order. I wasn't seeing it in my original test because "3" reversed the array back to it's original order but didn't find any value so it printed out "3." The answer was so painfully obvious, and yet totally hidden by my own expectations built on other languages.

FizzBuzzFactory = (x) => {
  if (x.length === 0) {
    return (num) => {return '' + num}
  }
  const funcArray = x.map(pair => maker(pair[0], pair[1])).reverse()
  
  return (num) => {
    let result = '';
    funcArray.forEach(func => {
      result = result.length > 0 ? result : func(num);
    });
    result = result.length > 0 ? result : '' + num;
    return result;
  }
}

This was my first working solution. It wasn't graceful, it wasn't pretty, but it accomplished the goal of the kata and my original goal of solving the problem using whatever I could from my FizzBuzz solution. And so I left it like this and moved on with my life.

For about 2 hours.

"I could do better than that."

Enter my 40 minute excursion to create a more clever solution. I knew it had to be possible, there were enough functional methods built into JS that it had to be. I gave myself 2 rules:

  1. The function can't do any mutation to the original array
  2. The function must be one line.

After a bit of thought I settled on an approach and started. My first iteration had problems, but it was a good start.

const fizzBuzzFactory = 
(arr) => (x) => arr.reverse().filter(v => x % v[0] === 0)[0][1] || `${x}`;

The obvious issue here was that accessing the filter result as [0][1] causes errors when there isn't any value after the filter (For example, 3 would return [] and not [[]] so [0] gives us undefined). To solve this problem I decided it would be easiest to make sure that a value would exist if I needed it to, and I accomplished this by concating an array value I knew would always end up in the array, the value itself.

const fizzBuzzFactory = 
(arr) => (x) => arr.concat([[x]]).filter(v => x % v[0] === 0)[0][1] || `${x}`;

Great! This prevented the errors, but it created another problem: we were getting the wrong response because we were looking at the first response rather than the last, meaning that instead of [6, "FizzBuzz"] getting used for 6, we were using [2, "Fizz"] since it's the first element in the array! This could be pretty easily fixed:

const fizzBuzzFactory = 
(arr) => (x) => arr.reverse().concat([[x]]).filter(v => x % v[0] === 0)[0][1] || `${x}`;

But now I was altering the original array (And in the process bringing back the problem from the original solution). I also couldn't move the concat before reverse() as it would leave the default value at the front of the array instead of the final value. A quick change brought me to my final solution:

const fizzBuzzFactory = 
(arr) => (x) => [[x]].concat(arr).reverse().filter(v => x % v[0] === 0)[0][1] || `${x}`;

Finally I had the graceful one-liner I was looking for.

Edit: I'm an idiot, here is a cleaner version:

const fizzBuzzFactory = arr => {
  return x => [[x, `${x}`]].concat(arr).reverse().filter(v => x % v[0] === 0)[0][1];
};

About the author

Photo by me, shot off the shore of Chicago