Home > Software engineering >  Best way of grouping array objects by multiple values in javascript.ES6
Best way of grouping array objects by multiple values in javascript.ES6

Time:12-28

Good day developers Im wondering how I could group an array of objects of different values in specific sub-groups , where in each sub-group my contain objects with specific values according to the key queried.

My array would be something like this

const cars = 
  [ { make: 'audi', model: 'r8',      year: '2012' } 
  , { make: 'audi', model: 'rs5',     year: '2013' } 
  , { make: 'ford', model: 'mustang', year: '2012' } 
  , { make: 'ford', model: 'fusion',  year: '2015' } 
  , { make: 'kia',  model: 'optima',  year: '2012' } 
  ] 

And i would like to gather by the key make in a subgroup of name 2nd_class all objects that in the key make, had as value kia or ford, gathering the others in a group 1rst_class Having as result an object like :

const expected = 
  [ '2nd_class': 
    [ { make: 'ford', model: 'mustang', year: '2012' } 
    , { make: 'ford', model: 'fusion',  year: '2015' } 
    , { make: 'kia',  model: 'optima',  year: '2012' } 
    ] 
  , '1rst_class' : 
    [ { make: 'audi', model: 'r8',  year: '2012' } 
    , { make: 'audi', model: 'rs5', year: '2013' } 
    ] 
  ]

All the examples on the web always refer to grouping by key and one specific value .... Any help would be amazing.

CodePudding user response:

You need to do something like that:

const cars = [
  {
    'make': 'audi',
    'model': 'r8',
    'year': '2012'
  }, {
    'make': 'audi',
    'model': 'rs5',
    'year': '2013'
  }, {
    'make': 'ford',
    'model': 'mustang',
    'year': '2012'
  }, {
    'make': 'ford',
    'model': 'fusion',
    'year': '2015'
  }, {
    'make': 'kia',
    'model': 'optima',
    'year': '2012'
  },
];

// Used car make
const usedMake = [];
// Return object
const formattedObject = {
  '1st_class': [],
  '2nd_class': []
};

// Iterate through your car array
cars.forEach(car => {
  // Check if this is the first time we see this make and process
  if (usedMake.indexOf(car.make) === -1) {
    // Retrieve the cars with the same make as our iterated car
    const filteredCars = cars.filter(c => c.make === car.make);
    
    if (['kia', 'ford'].includes(car.make)) {
      // push in our 2nd class - we push the retrieved objects
      formattedObject['2nd_class'].push(...filteredCars)
    } else {
      // push in our 1st class - we push the retrieved objects
      formattedObject['1st_class'].push(...filteredCars)
    }
    
    // Store the used car make so we don't reuse it later
    usedMake.push(car.make);
  }
});

console.log(formattedObject)

The kind of process iterate, check value unused, process if unused, store to prevent reuse is a basic programmation algorithm.

CodePudding user response:

this way :

const cars = 
  [ { make: 'audi', model: 'r8',      year: '2012' } 
  , { make: 'audi', model: 'rs5',     year: '2013' } 
  , { make: 'ford', model: 'mustang', year: '2012' } 
  , { make: 'ford', model: 'fusion',  year: '2015' } 
  , { make: 'kia',  model: 'optima',  year: '2012' } 
  ] 

const Class2 = [ 'ford', 'kia' ]

const expected = cars.reduce( (r,c) =>
  {
  let cls = Class2.includes(c.make) ? '2nd_class':'1rst_class'
  r[cls].push({...c})
  return r
  } , {'2nd_class':[],'1rst_class':[] }  ) 

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

CodePudding user response:

Rather than just write a one-off solution for your particular case, I decided to try to create a generic grouping function. Because you're trying to group, not only by a single value (the usual way utilities group things), this type of grouping function requires a bit more input.

So, I created the function:

groupBy(arr, propToGroup, mapping)

It takes the array of objects to group, the property within those objects to examine for the grouping and a mapping object that tells you which values for that property belong in which group name.

Here's a version you can run here in the snippet:

function groupBy(arr, propToGroup, mapping, defaultMapping) {
    let output = new Map();
    for (let item of arr) {
        // get value of our property of interest
        let val = item[propToGroup];
        if (val === undefined) {
            if (defaultMapping) {
                val = defaultMapping;
            } else {
                throw new Error(`No value for property .${propToGroup} and no defaultMapping`);
            }
        }
        let classification = mapping.get(val);
        if (!classification) {
            if (!defaultMapping) {
                throw new Error(`Property value ${val} is not present in mapping and no defaultMapping`);
            }
            classification = defaultMapping;
        }
        let classificationArray = output.get(classification);
        // if classification not found yet, then initialize as empty array
        if (!classificationArray) {
            classificationArray = [];
            output.set(classification, classificationArray);
        }
        classificationArray.push(item);
    }
    // convert to output format
    let result = [];
    for (let [key, val] of output) {
        result.push({
            [key]: val
        });
    }
    return result;
}

const cars = [
    { make: 'audi', model: 'r8', year: '2012' },
    { make: 'audi', model: 'rs5', year: '2013' },
    { make: 'ford', model: 'mustang', year: '2012' },
    { make: 'ford', model: 'fusion', year: '2015' },
    { make: 'kia', model: 'optima', year: '2012' },
    { make: 'vw', model: 'bug', year: '1960' },
];

const mapping = new Map([
    ['audi', '1rst_class'],
    ['ford', '2nd_class'],
    ['kia', '2nd_class']
]);

let result = groupBy(cars, "make", mapping, "other");
console.log(result);

The idea is that you could reuse this groupBy() function in other circumstances too. If a given property value is not found in the mapping and a defaultMapping was passed, then it will be put in the defaultMapping bucket. If no defaultMapping was passed and it's not in the mapping, it will throw and exception.

Note, the defaultMapping adds a number of lines of code, but attempts to handle the cases of unexpected data or data where you want a "catchall" bucket that catches everything else that isn't specific in the mapping. That obviously isn't required for your specific question, but likely makes this more generally useful for other situations.


Function explanation:

  1. Create a Map object for internal use to keep track of the groups encountered where the group name is the key and an array of objects in that group is the value for entry.

  2. Iterate through the array of objects.

  3. Get the property value of interest for the object.

  4. If the property doesn't exist, attempt to use the default mapping

  5. If the property does exist, look it up in the mapping to get its classification.

  6. If no classification is found, attempt to use the defaultMapping

  7. Lookup the classification in our temporary output Map

  8. If not found, then create the empty array for this classification.

  9. Add item to the classification array

  10. When done iterating the array, convert the internal Map object to the desired final array structure and return it.

CodePudding user response:

Or you can simply do this:

const cars = [ 
 { make: 'audi', model: 'r8',      year: '2012' }, 
 { make: 'audi', model: 'rs5',     year: '2013' }, 
 { make: 'ford', model: 'mustang', year: '2012' },
 { make: 'ford', model: 'fusion',  year: '2015' },
 { make: 'kia',  model: 'optima',  year: '2012' } 
];

const cars_in_classes=cars.reduce((a,c)=>{
  const cls=(c.make==="audi"?"1st":"2nd") "_class";
  (a[cls]=a[cls]||[]).push(c);
  return a;}, {} );

console.log(cars_in_classes);

The line (a[cls]=a[cls]||[]).push(c); checks whether the object property a[cls] already exists and - if not - creates it as an empty array before the current element is pushed onto it.

In case you consider several brands to be "1st_class" you could change line 2 to this:

const cls=(["audi","mercedes"].indexOf(c.make)>-1?"1st":"2nd") "_class";
  • Related