How to use reduce method to only get the max of each kind (in a 2d array)

Viewed 136

I would like to get the max of each type of bill using the reduce method. The function has to itirate and compare each item to the next in line, then if the names match(item[0]) it should compare their values (item[1]) and store the biggest value with that original name.

I want to use reduce for this but am struggling to understanding how exactly accumulator is being applied here. Any suggestions? :)

const tempCollected= [
    ["TWENTY", 20],
    ["TWENTY", 40],
    ["TWENTY", 60],
    ["TEN", 10],
    ["TEN", 20],
    ["FIVE", 5],
    ["FIVE", 10],
    ["FIVE", 15],
    ["ONE", 1],
    ["QUARTER", 0.25],
    ["QUARTER", 0.5],
    ["DIME", 0.1],
    ["DIME", 0.2],
    ["PENNY", 0.01],
    ["PENNY", 0.02],
    ["PENNY", 0.03]
]
/*  Desired outcome using reduce
    [
        ["TWENTY", 60],
        ["TEN", 20],
        ["FIVE", 15],
        ["ONE", 1],
        ["QUARTER", 0.5],
        ["DIME", 0.2],
        ["PENNY", 0.03]
    ]    
*/

/*My try */


const attempt=tempCollected.reduce(
    (a,b,i,arr)=>{
        if(b[i+1][0]===b[0]){   //if next item has the same name as the current
            return [...a,b[0],Math.max(b[1],b[i+1][1])] //return Math.max(...of those two) + the original name of the bill
        }
        return [...a,b]
    },[]
)

5 Answers

The way the problem appears to be set up requires a somewhat convoluted solution, since on each iteration, you may have to remove the previous element in the accumulator, or you may have to just add an element.

const tempCollected= [
    ["TWENTY", 20],
    ["TWENTY", 40],
    ["TWENTY", 60],
    ["TEN", 10],
    ["TEN", 20],
    ["FIVE", 5],
    ["FIVE", 10],
    ["FIVE", 15],
    ["ONE", 1],
    ["QUARTER", 0.25],
    ["QUARTER", 0.5],
    ["DIME", 0.1],
    ["DIME", 0.2],
    ["PENNY", 0.01],
    ["PENNY", 0.02],
    ["PENNY", 0.03]
];
const result = tempCollected.reduce((a, subarr) => {
  // if no items have been iterated over yet, or if the type is new,
  // push unconditionally
  if (
    !a.length || (
    a[a.length - 1][0] !== subarr[0]
  )) {
    a.push(subarr);
    return a;
  }
  // otherwise, remove the final item and push the new item
  // if the final item's value is greater
  if (subarr[1] > a[a.length - 1][1]) {
    a.pop();
    a.push(subarr);
  }
  return a;
}, []);
console.log(result);

If the method of solution isn't required to be reducing into an array, it would be much easier to group by turning it into an object instead.

const tempCollected= [
    ["TWENTY", 20],
    ["TWENTY", 40],
    ["TWENTY", 60],
    ["TEN", 10],
    ["TEN", 20],
    ["FIVE", 5],
    ["FIVE", 10],
    ["FIVE", 15],
    ["ONE", 1],
    ["QUARTER", 0.25],
    ["QUARTER", 0.5],
    ["DIME", 0.1],
    ["DIME", 0.2],
    ["PENNY", 0.01],
    ["PENNY", 0.02],
    ["PENNY", 0.03]
];
const grouped = {};
for (const [type, num] of tempCollected) {
  grouped[type] = Math.max((grouped[type] || 0), num);
}
const result = Object.entries(grouped);
console.log(result);

I don't think reduce is the way to go here (it could obviously be done because at the end you are still looping through the array, but it seems more like an abuse of the method). Don't be afraid to do things more verbose! Short code does not mean better code.

Take a look at the following snippet:

const data = [
    ['TWENTY', 20],
    ['TWENTY', 40],
    ['TWENTY', 60],
    ['TEN', 10],
    ['TEN', 20],
    ['FIVE', 5],
    ['FIVE', 10],
    ['FIVE', 15],
    ['ONE', 1],
    ['QUARTER', 0.25],
    ['QUARTER', 0.5],
    ['DIME', 0.1],
    ['DIME', 0.2],
    ['PENNY', 0.01],
    ['PENNY', 0.02],
    ['PENNY', 0.03]
];

const group = (input) => {
    const output = new Map();

    input.forEach((item) => {
        const [key, value] = item;
        output.set(key, Math.max(output.get(key) || -Infinity, value));
    });

    return Array.from(output.entries());
};

console.log(group(data));

Just for completion, this would be the reduce method 'abuse'. Essentially it is used to store the output object or map as the accumulated value and loop at the same time:

const data = [
    ['TWENTY', 20],
    ['TWENTY', 40],
    ['TWENTY', 60],
    ['TEN', 10],
    ['TEN', 20],
    ['FIVE', 5],
    ['FIVE', 10],
    ['FIVE', 15],
    ['ONE', 1],
    ['QUARTER', 0.25],
    ['QUARTER', 0.5],
    ['DIME', 0.1],
    ['DIME', 0.2],
    ['PENNY', 0.01],
    ['PENNY', 0.02],
    ['PENNY', 0.03]
];

const result = data.reduce((acc, item, index, input) => {
    acc[item[0]] = Math.max(acc[item[0]] || -Infinity, item[1]);
    if (index < input.length - 1) {
        return acc;
    } else {
        return Object.entries(acc);
    }
}, {});

console.log(result);

I initially thought you can use something like:

const max = tempCollected.reduce((max, current) => current[1] > max[1] ? current : max)

Then I realized you wanted all of them. What you're trying to do isn't possible with reduce (or it's not the way to do it).

Doing array.reduce on the array will only return ["TWENTY", 20] as the item on the iteration, it won't give you the entire group of items that you want to accumulate. You either have to manually do it or you have to restructure your array.

const items = {
  twenty: 0,
  ten: 0,
  five: 0, 
  one: 0,
  ...
}

collected.forEach((item) => {
  if (item[1] > items[item[0]]) {
    items[item[0]] = item[1]
  }
}

Or something like that depending on how you structure your data.

Can be a oneliner using Array.reduce to create an object and convert its result back to an Array with Object.entries. In the snippet the initial array is shuffled (unsorted), so an extra sort is added to sort the result descending on the result values. Note that using this reducer lambda the order of initial values has become irrelevant.

// shuffled the values a bit
const temp = [
  ["TWENTY", 60],
  ["FIVE", 5],
  ["PENNY", 0.03], 
  ["TWENTY", 40],
  ["TEN", 10],
  ["QUARTER", 0.5],
  ["TEN", 20],
  ["FIVE", 10],
  ["ONE", 1],  
  ["FIVE", 15],
  ["TWENTY", 20],
  ["QUARTER", 0.25],
  ["DIME", 0.1],
  ["PENNY", 0.01],
  ["DIME", 0.2],
  ["PENNY", 0.02], ];

const collectedMaxValues = Object.entries(
  temp.reduce( (acc, [key, val]) => 
    ( {...acc, [key]: (acc[key] || 0) > val ? acc[key] : val } ), {} )
).sort( ([,val1], [,val2]) => val2 - val1);

console.log(JSON.stringify(collectedMaxValues));

Maybe not the most elegant way, but this method doesn't rely on external variables to store end result, it converts reducer (a) into nested array on first step and with each iteration checks if the reducer already contains same name, if it does, updates the maximum value, otherwise adds a new item:

const tempCollected = [
  ["TWENTY", 20],
  ["TWENTY", 40],
  ["TWENTY", 60],
  ["TEN", 10],
  ["TEN", 20],
  ["FIVE", 5],
  ["FIVE", 10],
  ["FIVE", 15],
  ["ONE", 1],
  ["QUARTER", 0.25],
  ["QUARTER", 0.5],
  ["DIME", 0.1],
  ["DIME", 0.2],
  ["PENNY", 0.01],
  ["PENNY", 0.02],
  ["PENNY", 0.03]
]
/*  Desired outcome using reduce
    [
        ["TWENTY", 60],
        ["TEN", 20],
        ["FIVE", 15],
        ["ONE", 1],
        ["QUARTER", 0.5],
        ["DIME", 0.2],
        ["PENNY", 0.03]
    ]    
*/

const attempt = tempCollected.reduce((a, b, i) =>
{
  if (i == 1)
    a = [a];

  for (i = 0; i < a.length; i++)
  {
      if (a[i][0] == b[0])
        return a[i][1] = Math.max(a[i][1], b[1]), a;

  }
  return a.push(b), a;
});

console.log("attempt", attempt)

Related