Home > Enterprise >  Efficient way of counting number of matching objects within an array
Efficient way of counting number of matching objects within an array

Time:09-28

please see attempt and examples below, I am looking for an efficient way in JS to get the number of fruits matched. Currently I am using filter to find matches within items array, but I am wondering if there is a better approach (without having to filter through the fruits array on each iteration ~ imagining it is a much bigger array of fruits).

  const items = [
    {
      id: '111',
      name: 'apple',
    },
    {
      id: '222',
      name: 'apple',
    },
    {
      id: '333',
      name: 'kiwi',
    },
  ];

  const fruits = [
    {
      id: 'fruit-1',
      name: 'apple',
    },
    {
      id: 'fruit-2',
      name: 'banana',
    },
    {
      id: 'fruit-3',
      name: 'kiwi',
    },
  ];

  // attempted
  const numberOfFruitsInItems = fruits.map((fruit) => {
    const itemsWithFruit = items.filter(({ name }) => fruit.name === name);

    return {
      ...fruit,
      total: itemsWithFruit.length,
    };
  });

  console.log(numberOfFruitsInItems);

  // desired response
  [
    {
      id: 'fruit-1',
      name: 'apple',
      total: 2,
    },
    {
      id: 'fruit-2',
      name: 'banana',
      total: 0,
    },
    {
      id: 'fruit-3',
      name: 'kiwi',
      total: 1,
    },
  ];

CodePudding user response:

As many questions and answers on this site, you should group the array of objects by name, counting each and storing in a grouped object with keys that are names and values that are count.

const items = [{id:"111",name:"apple"},{id:"222",name:"apple"},{id:"333",name:"kiwi"},];
const fruits = [{id:"fruit-1",name:"apple"},{id:"fruit-2",name:"banana"},{id:"fruit-3",name:"kiwi"},];

var grouped = items.reduce(function(agg, item) {
  agg[item.name] = (agg[item.name] || 0)   1
  return agg
}, {})

fruits.forEach(function(item) {
  item.total = grouped[item.name] || 0
})

console.log(fruits)
.as-console-wrapper {max-height: 100% !important}

CodePudding user response:

You will need to cache the item frequencies before your map each of the fruits.

const
  items = [
    { id: '111', name: 'apple' },
    { id: '222', name: 'apple' },
    { id: '333', name: 'kiwi' }
  ],
  fruits = [
    { id: 'fruit-1', name: 'apple' },
    { id: 'fruit-2', name: 'banana' },
    { id: 'fruit-3', name: 'kiwi' }
  ];

const main = () => {
  const reducedFruits = fruitReducer(fruits, items);
  console.log(reducedFruits);
};

const computeFrequency = (items, accessor) =>
  items.reduce((acc, item) =>
    (key =>
      acc.set(key, (acc.get(key) ?? 0)   1))
    (accessor ? accessor(item) : item), new Map);

const fruitReducer = (fruits, items) =>
  ((freqMap) =>
    fruits.map(({ id, name }) =>
      ({ id, name, total: freqMap.get(name) ?? 0 })))
  (computeFrequency(items, ({ name }) => name))

main();
.as-console-wrapper { top: 0; max-height: 100% !important; }

CodePudding user response:

You can accomplish this in one iteration over items that updates a global counter of corresponding fruit.

// Use a hashed object to keep count of totals per fruit name.
// totals[FRUIT NAME] = ...number of items having name=FRUIT NAME
const totals = {}

// Fill the totals hash in one single iteration of items
items.forEach(item => {
  totals[item.name] = (totals[item.name] || 0)   1
})

const numberOfFruitsInItems = fruits.map((fruit) => {
  return {
    ...fruit,
    total: totals[fruit.name] || 0,
  };
});

CodePudding user response:

It would be better to separate out the two mechanisms to remove the nested iteration. First iterate over the items array using reduce to create a dictionary of key/value pairs (e.g. { apple: 2 }, and then loop over the fruits array adding in the correct count information from the dictionary.

const items=[{id:"111",name:"apple"},{id:"222",name:"apple"},{id:"333",name:"kiwi"}],fruits=[{id:"fruit-1",name:"apple"},{id:"fruit-2",name:"banana"},{id:"fruit-3",name:"kiwi"}];

// `reduce` over the items.
// If the item name isn't a key on the object
// set it to zero, then add one
const itemCount = items.reduce((acc, { name }) => {
  fruits[name] ??= 0;
    fruits[name];
  return fruits;
}, {});

// Update the `fruits` using the dictionary
// count information - note this mutates the
// `fruits` array so if you want a new array
// use `map` instead of `forEach`
fruits.forEach(fruit => {
  fruit.count = itemCount[fruit.name] || 0;
});

console.log(fruits);

Additional information

CodePudding user response:

A version which does not mutate your original data -- after all, we're not barbarians, right? -- could start by creating copies of your fruit array, adding a count property to each, storing them as an object keyed off the fruit name, then folding the items list into it by incrementing the count property of the relevant node. At the end, we just collect and return the values of this object.

The code is pretty simple:

const countFruits = (fruits, items) => Object .values (items .reduce (
  (a, {name}) => ((a [name] .count  = 1), a),
  fruits .reduce ((a, f) => ((a [f .name] = {...f , count: 0}), a), {})
))

const items = [{id: '111', name: 'apple'}, {id: '222', name: 'apple'}, {id: '333', name: 'kiwi'}]
const fruits = [{id: 'fruit-1', name: 'apple', }, {id: 'fruit-2', name: 'banana', }, {id: 'fruit-3', name: 'kiwi', }]

console .log (countFruits (fruits, items))
.as-console-wrapper {max-height: 100% !important; top: 0}

Note that there is no error-checking, and you may need it. What if items included {id" 444', name: 'papaya'}, even though paypaya was not in the list of fruits. This should not be too hard to handle, and we can leave it as an exercise.

  • Related