Rob Fyffe
Rob Fyffe

Reputation: 729

How to efficiently group an array of objects by a child property that is an array?

I am working on a function that takes an array of objects and returns an object with the data grouped by values of a nested array property:

INPUT

[
  {
    id: 0,
    section: ['valueX']
  },
  {
    id: 1,
    section: ['valueX']
  },
  {
    id: 2,
    section: ['valueY']
  },
  {
    id: 3,
    section: ['valueY', 'valueX', 'valueZ']
  },
  {
    id: 4,
    section: []
  }
];

OUTPUT


{
  valueX: [{...}]
  valueY: [{...}]
  valueZ: [{...}],
  all: [{...}]
}

I have this working using reduce and a couple of nested forEach loops. I wanted to see what other alternatives are out there. I do not want to use any library or implement extremely unreadable code.

MY ATTEMPT

const groupBy = ({ data, property }) =>
  data.reduce((arr, current) => {
    const result = arr;
    const groupList = current[property];
    const groupHasItems = groupList.length > 0;
    const groupItemExists = result[groupList];

    if (groupHasItems && !groupItemExists) {
      groupList.forEach(id => {
        if (!result[id]) {
          result[id] = [];
        }
      });
    } else if (!result.all) {
      result.all = [];
    }

    if (groupHasItems) {
      groupList.forEach(id => {
        result[id].push(current);
      });
    } else {
      result.all.push(current);
    }

    return result;
  }, {});

Upvotes: 1

Views: 296

Answers (1)

Alex L
Alex L

Reputation: 4241

I have a recursive way which could handle deeper nesting etc., which doesn't seem like it's needed for you(?) but it's there if you want it. If you only want 1 level deep, then we can also simplify it.

const input = [
  {id: 0, section: ['valueX']},
  {id: 1, section: ['valueX']},
  {id: 2, section: ['valueY']},
  {id: 3, section: ['valueY', 'valueX', 'valueZ']},
  {id: 4, section: []}
];

const groupBy = (input, propertyArr) => {
  //console.log(propertyArr);
  const property = propertyArr[0];
  const grouped = input.reduce((groupedObj, item) => {
    const key = item[property];
    if (key instanceof Array && key.length > 0){
      key.forEach(k => {
        groupedObj[k] = [...(groupedObj[k] || []), item];
      })
    }else if (key instanceof Array){
      groupedObj['all'] = [...(groupedObj['all'] || []), item];
    }else{
      groupedObj[key] = [...(groupedObj[key] || []), item];
    }        
    return groupedObj;
  }, {});
  if (propertyArr.length > 1) {
    //console.log(grouped);
    return Object.keys(grouped).reduce((AggObj, key, index) => {
      const propertyArrCopy = [...propertyArr];
      propertyArrCopy.shift();
      AggObj[key] = groupBy(grouped[key], propertyArrCopy);
      return AggObj;
    }, {});
  }else {
    return grouped;
  }
};

const grouped = groupBy(input, ["section"]);
console.log(grouped);

const inputNested = [
  {id: 0, somekey: 'someValA', section: ['valueX']},
  {id: 1, somekey: 'someValB', section: ['valueX']},
  {id: 2, somekey: 'someValA', section: ['valueY']},
  {id: 3, somekey: 'someValB', section: ['valueY', 'valueX', 'valueZ']},
  {id: 4, somekey: 'someValA', section: []}
];

//Nested Example
//const nestedGrouped = groupBy(inputNested, ["somekey", "section"]);
//console.log(nestedGrouped);
.as-console-wrapper { max-height: 100% !important; top: 0; }

The simplified code for your specific 1-level deep problem:

const input = [
  {id: 0, section: ['valueX']},
  {id: 1, section: ['valueX']},
  {id: 2, section: ['valueY']},
  {id: 3, section: ['valueY', 'valueX', 'valueZ']},
  {id: 4, section: []}
];

const groupBy = (input, propertyArr) => {
  //console.log(propertyArr);
  const property = propertyArr[0];
  const grouped = input.reduce((groupedObj, item) => {
    const key = item[property];
    if (key instanceof Array && key.length > 0){
      key.forEach(k => {
        groupedObj[k] = [...(groupedObj[k] || []), item];
      })
    }else{
      groupedObj['all'] = [...(groupedObj['all'] || []), item];
    }    
    return groupedObj;
  }, {});
  
  return grouped;
};

const nestedGrouped = groupBy(input, ["section"]);

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

Here is your code for reference:

const input = [
  {id: 0, section: ['valueX']},
  {id: 1, section: ['valueX']},
  {id: 2, section: ['valueY']},
  {id: 3, section: ['valueY', 'valueX', 'valueZ']},
  {id: 4, section: []}
];


const groupBy = ({ data, property }) =>
  data.reduce((arr, current) => {
    const result = arr;
    const groupList = current[property];
    const groupHasItems = groupList.length > 0;
    const groupItemExists = result[groupList];

    if (groupHasItems && !groupItemExists) {
      groupList.forEach(id => {
        if (!result[id]) {
          result[id] = [];
        }
      });
    } else if (!result.all) {
      result.all = [];
    }

    if (groupHasItems) {
      groupList.forEach(id => {
        result[id].push(current);
      });
    } else {
      result.all.push(current);
    }

    return result;
  }, {});
  
  console.log(groupBy({ data: input, property: 'section'}))
.as-console-wrapper { max-height: 100% !important; top: 0; }

Upvotes: 1

Related Questions