Reducing Katas

Currently back doing kata as I realise after a year of glue engineering, a lot of heavy React and a lot of Node, I’ve forgotten a lot of the basics. There are certain programming practices, I feel, that are essential to keep sharp - it’s all too easy to get out of the habit of these though. So I decided to make a concerted effort to get back into the habit of doing kata, and boy did I learn quickly how much I’ve forgotten.

The Problem

For this particular kata, Update inventory in your smartphone store, we need to merge two inventory arrays. Each array contains items in the format [quantity, itemName], and if an item exists in both arrays, we need to sum the quantities.

Piece of cake.

Here’s the test case for us to pass:

const Test = require('@codewars/test-compat');

describe("Tests", () => {
  it("test", () => {
var currentInventory = [
  [25, 'HTC'],
  [1000, 'Nokia'],
  [50, 'Samsung'],
  [33, 'Sony'],
  [10, 'Apple']
];

var newInventory = [
  [5, 'LG'],
  [10, 'Sony'],
  [4, 'Samsung'],
  [5, 'Apple']
];

Test.assertSimilar(updateInventory(currentInventory, newInventory), [[15,'Apple'],[25,'HTC'],[5,'LG'],[1000,'Nokia'],[54,'Samsung'],[43,'Sony']]);
  });
});

Initial Thoughts

I set out to solve this within half hour, and embarrassingly took me nearly an hour as I not only had to look up the reduce documentation, but also refresh myself on Sets, Maps, and other data structures. Kyu 6. No worries 🥋.

I also find reduce the hardest array method to work with and seem to always forget how best to use it. I’m hell bent on using it for most katas’ at the minute because I want to improve my functional programming skills. .reduce() is also the most powerful array method in JavaScript, in that you can recreate .map(), .filter(), .find(), .sort() and others using it. It’s a real utility method that I often overlook in favour of the simpler array methods.

When I first approached the problem, I did quickly work out a solution using traditional for loops. Then I tried to refactor it to use reduce(). I figured I could also potentially use Set or Map in some fancy way to merge the two arrays while totalling the inventory quantities. In a real-world scenario, I’d probably use a Map for this kind of operation, but I wanted to practice reduce() specifically.

Long and short of it is I purposefully got in my own way choosing to use reduce() and not just solve the problem. I’m glad I did though, as I’ve learnt (or rather, remembered/relearnt) a lot from just this one simple kata.

Broken Solution

Here was my first attempt using reduce():

function updateInventory(curStock, newStock) {
  const updatedStock = newStock.reduce((acc, curValue) => {
    const found = acc.findIndex((el) => el[1] === curValue[1]);
    console.log('found', found);
    
    if (found >= 0) {
      return [...acc, [curValue[0] + acc[found][0], curValue[1]]];
    }
   
    return [...acc, curValue];
    
  }, curStock).sort((a, b) => a[1].localeCompare(b[1]));
  
  return updatedStock;
}

Which will give you:

[
  [ 10, 'Apple' ],
  [ 15, 'Apple' ],
  [ 25, 'HTC' ],
  [ 5, 'LG' ],
  [ 1000, 'Nokia' ],
  [ 50, 'Samsung' ],
  [ 54, 'Samsung' ],
  [ 33, 'Sony' ],
  [ 43, 'Sony' ]
]

I’m glad in hindsight I was straight on to the right path by using the curStock as the initial value for the accumulator. This is a good practice when you want to update an existing array, rather than creating a new one from scratch. And during a few passes I did switch to using and empty array to see if that would be easier.

Anyway, this results in everything getting added, but not properly updated. The original items were still in the accumulator, creating duplicates. The issue is that I was spreading the entire accumulator (…acc) and then adding a new updated item, rather than replacing the existing one.

Working Solution

The key was to create an intermediate copy of the accumulator array and update the specific item at the found index:

function updateInventory(curStock, newStock) {  
  const updatedStock = newStock.reduce((acc, newItem) => {    
    const found = acc.findIndex((el) => el[1] === newItem[1]);
    
    if (found >= 0) {
      const updatedAcc = [...acc];
      updatedAcc[found] = [newItem[0] + acc[found][0], newItem[1]];
      return updatedAcc;
    }
   
    return [...acc, newItem];
  }, curStock).sort((a, b) => a[1].localeCompare(b[1]));
  
  return updatedStock;
}

Immutability and patterns you’ve never heard of, but totally exist.

Along the way I remembered the importance of immutability in JavaScript. And I realised that I was going about this in the wrong way by mutating the accumulator array. As soon as I realised this, I knew I had to create a copy of the accumulator array before modifying it - this also had the added side effect of making the code more readable and easy to follow, as well as easier to debug.

This is now referred to as the “copy, modify, return”, pattern citation needed, and is fundamental in functional programming.

Instead of modifying the original array directly (mutation), we create a new copy, make our changes, and return the new version. You see similar patterns in Redux, React, and other modern JavaScript libraries all the time, and generally if performance and memory isn’t a concern (we’re using JS after all), it’s a good practice to follow.

Alternative Approach Using Map

For comparison, here’s how you might solve it using Map:

function updateInventory(curStock, newStock) {
  const inventoryMap = new Map();

  curStock.forEach(([qty, name]) => {
    inventoryMap.set(name, qty);
  });
  
  newStock.forEach(([qty, name]) => {
    if (inventoryMap.has(name)) {
      inventoryMap.set(name, inventoryMap.get(name) + qty);
    } else {
      inventoryMap.set(name, qty);
    }
  });
  
  return Array.from(inventoryMap)
    .map(([name, qty]) => [qty, name])
    .sort((a, b) => a[1].localeCompare(b[1]));
}

Which, while a bit more readable, doesn’t really offer much else in terms of performance or efficiency, and you still have to convert it back to an array at the end anyway to satisfy the test case.

Lessons Learned

I’ve learnt that holy hell, am I rusty 🤣 … but, it’s been nice relearning .reduce() and also remembering localeCompare() which I had completely forgotten about for comparing strings alphabetically (i had to look that up too!).

Where array methods like .map(), .filter(), and .sort() are relatively straightforward, and easy to remember (map transforms each element, filter removes elements, sort sorts elements), .reduce() is a bit more complex and requires a bit more thought. My mental model is to think of a ‘start’ value, and then how that value (the accumulator) is transformed at each step of the reducer function, with the output being the final result. I could probably think of some way better eli5 analogy here but at this point I’m out of fresh ideas, and coffee.

So yeah, the biggest takeaway for me here is the importance of understanding how data is transformed at each step of a reducer function. If I can remember that, then I can remember exactly how a reducer works. On to the next one…