Group array of objects by multiple nested values

Viewed 69

I have an array of objects which presents tasks. These tasks are categorized (primary / secondary category).

let tasks = [
  {
    id: 1,
    name: 'Cleanup desk',
    primary_category: {
      id: 1,
      name: 'Indoor'
    },
    secondary_category: {
      id: 2,
      name: 'Surfaces'
    }
  },
  {
    id: 2,
    name: 'Cleanup office floors',
    primary_category: {
      id: 1,
      name: 'Indoor'
    },
    secondary_category: {
      id: 3,
      name: 'Ground'
    }
  },
  {
    id: 3,
    name: 'Water plants',
    primary_category: {
      id: 2,
      name: 'Outdoor'
    },
    secondary_category: {
      id: 3,
      name: 'Irrigation'
    }
  }
];

I now try to create a categories accordion in my frontend and therefore need to group my array differently. The structure should look like:

1) primary category 
   > secondary category 
      > tasks
   > secondary category 
      > tasks
2) primary category 
   > secondary category 
      > tasks

Therefore I'm trying to achieve a structure similar to this:

let tasks_categorized = [
  {
    id: 1,
    name: 'Indoor',
    secondary_categories: [
      {
        id: 2,
        name: 'Surfaces',
        tasks: [
          {
            id: 1,
            name: 'Cleanup desk'
          }
        ]
      },
      {
        id: 3,
        name: 'Ground',
        tasks: [
          {
            id: 2,
            name: 'Cleanup office floors'
          }
        ]
      }
    ]
  },
  {
    id: 2,
    name: 'Outdoor',
    secondary_categories: [
      {
        id: 3,
        name: 'Irrigation',
        tasks: [
          {
            id: 3,
            name: 'Water plants'
          }
        ]
      }
    ]
  }
];

I tried using groupBy by lodash but this does not allow grouping by multiple nested key-value pairs. Does anybody know an approach to solve this?

Thank you in advance!

3 Answers

The following provided approach is going to achieve the expected result within a single reduce cycle without any further nested loops.

It does so by implementing a reducer function which creates and/or aggregates at time a prioritized category task while iterating another task array. But most importantly it keeps track of a task item's related primary and secondary categories via a Map based lookup. This lookup reference together with a result array are properties of this function's return value which has to be partly provided as the reduce method's initial value as follows ... { result: [] }.

function createAndAggregatePrioritizedCategoryTask(
  { lookup = new Map, result }, item
) {
  const { primary_category, secondary_category, ...taskRest } = item;

  const { id: primaryId, name: primaryName } = primary_category;
  const { id: secondaryId, name: secondaryName } = secondary_category;

  const primaryKey = [primaryId, primaryName].join('###');
  const secondaryKey = [primaryKey, secondaryId, secondaryName].join('###');

  let primaryCategory = lookup.get(primaryKey);
  if (!primaryCategory) {

    // create new primary category item.
    primaryCategory = {
      id: primaryId,
      name: primaryName,
      secondary_categories: [],
    };
    // store newly created primary category reference in `lookup`.
    lookup.set(primaryKey, primaryCategory);

    // push newly created primary category reference to `result`.
    result.push(primaryCategory);
  }
  let secondaryCategory = lookup.get(secondaryKey);
  if (!secondaryCategory) {

    // create new secondary category item.
    secondaryCategory = {
      id: secondaryId,
      name: secondaryName,
      tasks: [],
    };
    // store newly created secondary category reference in `lookup`.
    lookup.set(secondaryKey, secondaryCategory);

    // push newly created secondary category reference into the
    // `secondary_categories` array of its related primary category.
    primaryCategory
      .secondary_categories
      .push(secondaryCategory);
  }
  // push the currently processed task-item's rest-data as 
  // item into the related secondary category's `task` array.
  secondaryCategory
    .tasks
    .push(taskRest);

  return { lookup, result };
}

let tasks = [{
  id: 1,
  name: 'Cleanup desk',
  primary_category: { id: 1, name: 'Indoor' },
  secondary_category: { id: 2, name: 'Surfaces' },
}, {
  id: 2,
  name: 'Cleanup office floors',
  primary_category: { id: 1, name: 'Indoor' },
  secondary_category: { id: 3, name: 'Ground' },
}, {
  id: 3,
  name: 'Water plants',
  primary_category: { id: 2, name: 'Outdoor' },
  secondary_category: { id: 3, name: 'Irrigation' },
}];

const { result: tasks_categorized } = tasks
  .reduce(createAndAggregatePrioritizedCategoryTask, { result: [] });

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

You could take a dynamic approach with an array of arrays with functions and keys for the nested arrays.

const
    tasks = [{ id: 1, name: 'Cleanup desk', primary_category: { id: 1, name: 'Indoor' }, secondary_category: { id: 2, name: 'Surfaces' } }, { id: 2, name: 'Cleanup office floors', primary_category: { id: 1, name: 'Indoor' }, secondary_category: { id: 3, name: 'Ground' } }, { id: 3, name: 'Water plants', primary_category: { id: 2, name: 'Outdoor' }, secondary_category: { id: 3, name: 'Irrigation' } }],
    groups = [
        [o => o, 'primary category'],
        [o => o.primary_category, 'secondary category'],
        [o => o.secondary_category, 'tasks']
    ],
    result = tasks.reduce((r, o) => {
        groups.reduce((parent, [fn, children]) => {
            const { id, name } = fn(o);
            let item = (parent[children] ??= []).find(q => q.id === id)
            if (!item) parent[children].push(item = { id, name });
            return item;
        }, r);
        return r;
    }, {})[groups[0][1]];

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

might not be the cleanest solution, but this will work

const tasks = [{
    id: 1,
    name: "Cleanup desk",
    primary_category: {
      id: 1,
      name: "Indoor"
    },
    secondary_category: {
      id: 2,
      name: "Surfaces"
    }
  },
  {
    id: 2,
    name: "Cleanup office floors",
    primary_category: {
      id: 1,
      name: "Indoor"
    },
    secondary_category: {
      id: 3,
      name: "Ground"
    }
  },
  {
    id: 3,
    name: "Water plants",
    primary_category: {
      id: 2,
      name: "Outdoor"
    },
    secondary_category: {
      id: 3,
      name: "Irrigation"
    }
  }
];

let fixedCategories = [];
tasks.map((object) => {
  const categoryIndex = fixedCategories.findIndex(
    (category) => category.name === object.primary_category.name
  );
  if (categoryIndex >= 0) {
    const subCategoryIndex = fixedCategories[
      categoryIndex
    ].secondary_categories.findIndex(
      (subCategory) => subCategory.name === object.secondary_category.name
    );
    if (subCategoryIndex >= 0) {
      fixedCategories[categoryIndex].secondary_categories[
        subCategoryIndex
      ].tasks.push({
        id: object.id,
        name: object.name
      });
    } else {
      const subCategory = {
        name: object.secondary_category.name,
        id: object.secondary_category.id,
        tasks: [{
          id: object.id,
          name: object.name
        }]
      };
      fixedCategories[categoryIndex].secondary_categories.push(subCategory);
    }
  } else {
    const category = {
      name: object.primary_category.name,
      id: object.primary_category.id,
      secondary_categories: [{
        name: object.secondary_category.name,
        id: object.secondary_category.id,
        tasks: [{
          id: object.id,
          name: object.name
        }]
      }]
    };
    fixedCategories.push(category);
  }
  return fixedCategories;
});

console.log(fixedCategories);

Related