0

I have an array of objects taking this shape...

type allRecipes = {
  rows: [
    {
      category: string;
      id: number;
      owner: string;
      recipes_uri: string;
      recipes_name: string;
    }
  ];
};

Many recipes have the same recipes_name and the same category but with different id, owner and recipes_uri.

I need to collate these into this new shape that will remove some of the duplications and make the data easier to handle.

type recipesCollated = [
 {
  category: string;
  recipes_name: string;
  recipes_collection: [
    {
      id: number;
      owner: string;
      recipes_uri: string;
    }
   ];
  }
 ];

So I'm trying to loop over allRecipes.rows then should I use .reduce I've stubbed out some sudo code in the comments...

const recipesCollated = [];
for (let i = 0; i < allRecipes.rows.length; i++) { 
  // is allRecipes.rows[i].recipes_name in the recipesCollated array??;
  // if its not push a new record in with one item in the recipes_collection array
  // if it is, loop over recipesCollated.recipes_collection and check to see if the current id is in the array
  // if it is, our job is done, if its not insert it into recipesCollated.recipes_collection array
}
Bill
  • 4,614
  • 13
  • 77
  • 132
  • Does this answer your question? [How to group an array of objects based on multiple keys in Javascript?](https://stackoverflow.com/questions/54624567/how-to-group-an-array-of-objects-based-on-multiple-keys-in-javascript) – pilchard Jul 20 '21 at 21:58
  • it's close it uses a single key but I have two keys `category` and `recipes_name` ill certainly consider it and see if I can adapt it – Bill Jul 20 '21 at 22:02
  • It uses three keys actually *'I want to group it by WarehouseId, ShippingCarrierId and PostalCodeType'* you really just have to type in your property names and rename `PickupTimeSlots` to `recipes_collection` and it works out of the box. – pilchard Jul 20 '21 at 22:05

3 Answers3

2

Rather than checking for inclusion in an array (which is O(n^2)), it's better to use maps associating category and name to an array of entries. You can always convert it to an array if necessary. For example:

const recipesCollated_ = new Map();

for (const recipe of recipes.rows) {
    let category_map = recipesCollated_.get(recipe.category);
    if (typeof category_map === 'undefined') {
        const new_map = new Map();
        recipesCollated_.set(recipe.category, new_map);
        category_map = new_map;
    }
    let recipe_array = category_map.get(recipe.recipes_name);
    if (typeof recipe_array === 'undefined') {
        const new_arr = [];
        category_map.set(recipe.recipes_name, new_arr);
        recipe_array = new_arr;
    }
    recipe_array.push({ recipes_uri: recipe.recipes_uri, owner: recipe.owner, id: recipe.id, });
}

const recipesCollated = [];
for (const [ category, recipes_map, ] of recipesCollated_) {
    for (const [ recipe_name, recipes, ] of recipes_map) {
        recipesCollated.push({ recipes_name: recipe_name, category: category, recipes_collection: recipes, });
    }
}
  • REPL: https://replit.com/@mattbaconbacon/JampackedCyanHexagon –  Jul 20 '21 at 22:23
  • 1
    Addendum: while this code is certainly longer than it could be (especially if it used objects rather than maps), this is more proper and robust. –  Jul 20 '21 at 22:26
1

This can be handled with a single Array#reduce() call using a composite key of the relevant properties to group by.

With a Map

const allRecipes = { rows: [{ category: 'cat 1', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 1', }, { category: 'cat 2', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 5', }, { category: 'cat 1', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 1', }, { category: 'cat 2', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 5', }, { category: 'cat 1', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 23', }], };

const collatedRecipes = [...allRecipes.rows.reduce((acc, { category, recipes_name, ...rest }) => {
  const key = `${category}_${recipes_name}`;
  if (acc.has(key)) {
    acc.get(key).recipes_collection.push(rest);
  } else {
    acc.set(key, { category, recipes_name, recipes_collection: [rest] })
  }
  return acc;
}, new Map)
  .values()]

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

Or an object

const allRecipes = { rows: [{ category: 'cat 1', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 1', }, { category: 'cat 2', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 5', }, { category: 'cat 1', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 1', }, { category: 'cat 2', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 5', }, { category: 'cat 1', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 23', }], };

const collatedRecipes = Object
  .values(allRecipes.rows
    .reduce((acc, { category, recipes_name, ...rest }) => (
      (acc[`${category}_${recipes_name}`] ??= { category, recipes_name, recipes_collection: [] })
        .recipes_collection.push(rest), acc)
      , {})
  );

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

Or a Map and a for...of loop

const allRecipes = { rows: [{ category: 'cat 1', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 1', }, { category: 'cat 2', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 5', }, { category: 'cat 1', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 1', }, { category: 'cat 2', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 5', }, { category: 'cat 1', id: 1, owner: 'Bill', recipes_uri: 'uri2', recipes_name: 'recipe 23', }], };

const collatedMap = new Map;

for (const { category, recipes_name, ...rest } of allRecipes.rows) {
  const key = `${category}_${recipes_name}`;
  if (collatedMap.has(key)) {
    collatedMap.get(key).recipes_collection.push(rest);
  } else {
    collatedMap.set(key, { category, recipes_name, recipes_collection: [rest] });
  }
}

const collatedArray = [...collatedMap.values()];

console.log(collatedArray);
.as-console-wrapper { max-height: 100% !important; top: 0; }
pilchard
  • 12,414
  • 5
  • 11
  • 23
  • I get this ts error: Type 'IterableIterator' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators. – Bill Jul 21 '21 at 09:07
  • See: [Why `downlevelIteration` is not on by default?](https://stackoverflow.com/questions/53441292/why-downleveliteration-is-not-on-by-default) and a post linked from one of the answers [Downlevel Iteration for ES3/ES5 in TypeScript](https://mariusschulz.com/blog/downlevel-iteration-for-es3-es5-in-typescript) – pilchard Jul 21 '21 at 09:15
0

Simply running a reduce method on your original array and checking if name and category exist. If so, push the current recipe in existing object, else add a new object in the accumulated result.

const recipes = {
  rows: [
    {
      category: 'c1',
      id: 1,
      owner: 'o1',
      recipes_uri: 'some url',
      recipes_name: 'rn-1',
    },
    {
      category: 'c1',
      id: 2,
      owner: 'o2',
      recipes_uri: 'some url-2',
      recipes_name: 'rn-1'
    }
  ]
};

const result = recipes.rows.reduce((acc, cur) => {
  const {recipes_name, category, ...recipe} = cur;
  const recipeNameExist = acc.find(rec => rec.recipes_name === recipes_name);
  const recipeCatExist = acc.find(rec => rec.category === category);

  if(!(recipeCatExist && recipeNameExist)) {
    acc.push({
      recipes_name,
      category,
      recipes_collection: [recipe]
    })
  } else {
    recipeCatExist.recipes_collection.push(recipe)
  }
  return acc;

}, []);

console.dir(result);

/*

[
  {
    recipes_name: 'rn-1',
    category: 'c1',
    recipes_collection: [
      { id: 1, owner: 'o1', recipes_uri: 'some url' },
      { id: 2, owner: 'o2', recipes_uri: 'some url-2' }
    ]
  }
]

*/

Working REPL

AdityaParab
  • 7,024
  • 3
  • 27
  • 40
  • This is a clear duplicate with a more concise answer. – pilchard Jul 20 '21 at 22:07
  • how to check if the current id is already in recipesCollated.recipes_collection ?? I don't want to push duplicates in there – Bill Jul 20 '21 at 22:11
  • @Bill: before pushing, just do `const doesIdExist = recipeCatExist.recipes_collection.find(rec => rec.id === cur.id);` if `doesIdExist` is not undefined, push `cur` – AdityaParab Jul 20 '21 at 22:17