Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Group Array of JavaScript Objects by Key or Property Value

Group array of JavaScript objects by keys

This fork of JamieMason's implementation changes the key parameter to be an array of keys instead of just a single key. This makes it possible to group by multiple properties instead of just one.

Implementation

const groupBy = keys => array =>
  array.reduce((objectsByKeyValue, obj) => {
    const value = keys.map(key => obj[key]).join('-');
    objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
    return objectsByKeyValue;
  }, {});
Click to see TypeScript version
/**
 * Group array of objects by given keys
 * @param keys keys to be grouped by
 * @param array objects to be grouped
 * @returns an object with objects in `array` grouped by `keys`
 * @see <https://gist.github.com/mikaello/06a76bca33e5d79cdd80c162d7774e9c>
 */
const groupBy = <T>(keys: (keyof T)[]) => (array: T[]): Record<string, T[]> =>
  array.reduce((objectsByKeyValue, obj) => {
    const value = keys.map((key) => obj[key]).join('-');
    objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
    return objectsByKeyValue;
  }, {} as Record<string, T[]>);

Usage

const cars = [
  { brand: 'Audi', produced: '2016', color: 'black' },
  { brand: 'Audi', produced: '2017', color: 'white' },
  { brand: 'Ford', produced: '2016', color: 'red' },
  { brand: 'Ford', produced: '2016', color: 'white' },
  { brand: 'Peugot', produced: '2018', color: 'white' }
];

const groupByBrand = groupBy(['brand']);
const groupByColor = groupBy(['color']);
const groupByBrandAndYear = groupBy(['brand', 'produced']);

console.log(
  JSON.stringify({
    carsByBrand: groupByBrand(cars),
    carsByColor: groupByColor(cars),
    carsByBrandAndYear: groupByBrandAndYear(cars)
  }, null, 2)
);

Output

{
  "carsByBrand": {
    "Audi": [
      {
        "brand": "Audi",
        "produced": "2016",
        "color": "black"
      },
      {
        "brand": "Audi",
        "produced": "2017",
        "color": "white"
      }
    ],
    "Ford": [
      {
        "brand": "Ford",
        "produced": "2016",
        "color": "red"
      },
      {
        "brand": "Ford",
        "produced": "2016",
        "color": "white"
      }
    ],
    "Peugot": [
      {
        "brand": "Peugot",
        "produced": "2018",
        "color": "white"
      }
    ]
  },
  "carsByColor": {
    "black": [
      {
        "brand": "Audi",
        "produced": "2016",
        "color": "black"
      }
    ],
    "white": [
      {
        "brand": "Audi",
        "produced": "2017",
        "color": "white"
      },
      {
        "brand": "Ford",
        "produced": "2016",
        "color": "white"
      },
      {
        "brand": "Peugot",
        "produced": "2018",
        "color": "white"
      }
    ],
    "red": [
      {
        "brand": "Ford",
        "produced": "2016",
        "color": "red"
      }
    ]
  },
  "carsByBrandAndYear": {
    "Audi-2016": [
      {
        "brand": "Audi",
        "produced": "2016",
        "color": "black"
      }
    ],
    "Audi-2017": [
      {
        "brand": "Audi",
        "produced": "2017",
        "color": "white"
      }
    ],
    "Ford-2016": [
      {
        "brand": "Ford",
        "produced": "2016",
        "color": "red"
      },
      {
        "brand": "Ford",
        "produced": "2016",
        "color": "white"
      }
    ],
    "Peugot-2018": [
      {
        "brand": "Peugot",
        "produced": "2018",
        "color": "white"
      }
    ]
  }
}

See playcode.io for example.

@MaheshStark
Copy link

MaheshStark commented Apr 5, 2020

is there any way to get the count which group have

@mikaello
Copy link
Author

mikaello commented Apr 5, 2020

is there any way to get the count which group have

Yes, that is possible. The values of a group is just an array, so you could just check the length property of that array:

const groupBy = (keys) => (array) =>
  array.reduce((objectsByKeyValue, obj) => {
    const value = keys.map((key) => obj[key]).join("-");
    objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
    return objectsByKeyValue;
  }, {});

const cars = [
  { brand: "Audi", produced: "2016", color: "black" },
  { brand: "Audi", produced: "2017", color: "white" },
  { brand: "Ford", produced: "2016", color: "red" },
  { brand: "Ford", produced: "2016", color: "white" },
  { brand: "Peugot", produced: "2018", color: "white" },
];

const groupByBrandAndYear = groupBy(["brand", "produced"]);

for (let [groupName, values] of Object.entries(groupByBrandAndYear(cars))) {
  console.log(`${groupName}: ${values.length}`);
}
# console output
Audi-2016: 1
Audi-2017: 1
Ford-2016: 2
Peugot-2018: 1

@maheshUni2020
Copy link

maheshUni2020 commented Apr 5, 2020

Thank it is very useful to me

@phil-epl
Copy link

phil-epl commented Jun 19, 2020

Is there any way to construct the function without Arrow functions? Confused how to deconstruct into a "normal" function but unable to use arrow functions in my implementation @mikaello

@mikaello
Copy link
Author

mikaello commented Jun 20, 2020

@eplPA: You can convert arrow function to old school function this way:

  • (param) => val: function(param) { return val }
  • (param) => { return val }: function(param) { return val }

Read more about arrow functions on MDN

@phil-epl
Copy link

phil-epl commented Jun 22, 2020

Thanks for the response @mikaello
I understand the basics of converting arrow functions, I am just having trouble visualizing this one specifically because there are multiple arrows.

@mikaello
Copy link
Author

mikaello commented Jun 22, 2020

I see. It is exactly the same concept as with a single arrow, just more function():

a => b => c
// Is the same as
function(a) { 
  return function(b) { 
    return c
  }
}

This is a technique called currying. There are lots of articles about JavaScript and currying, e.g. Currying in Javascript — arrow function sequence
(which has examples with both function() and arrow-function).

@phil-epl
Copy link

phil-epl commented Jun 24, 2020

EDIT** I actually think I got it... if anyone needs an non-arrow function version this seems to be working for me:

function groupBy(keys) {
  return function(array) {
    return array.reduce(function(objectsByKeyValue, obj) {
      var value = keys.map(function(key){return (obj[key])}).join('-');
      objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
      return objectsByKeyValue; 
    }, {})
  } 
}

@mikaello
Copy link
Author

mikaello commented Jun 24, 2020

I'm glad you figured it out :-)

@MaheshStark
Copy link

MaheshStark commented Jun 25, 2020

Thank you

@MaccerC
Copy link

MaccerC commented Jul 29, 2020

@mikaello In my code I use groupArray=require('group-array) which allows me to group an array on multiple properties. Your code does the same, but with a slightly different output.
You're concatenate the 2 keys like so: "Audi - 2016". What I want as outcome is: { "Audi": { 2016: [{...}] } }
Is this format also possible with your code?

@mikaello
Copy link
Author

mikaello commented Jul 30, 2020

@MaccerC

Is this format also possible with your code?

With a modification of how the key is created it is possible.

Group by with a nested structure:

const groupBy = (keys) => (array) =>
  array.reduce((objectsByKeyValue, obj) => {
   // Instead of creating a unique key for each grouped by values, we are now traversing (and building) 
   // the whole object structure for every array value:
    keys.reduce((builder, key, index) => {
      if (index !== keys.length - 1) {
        // Building the nested grouped by structure
        builder[obj[key]] = builder[obj[key]] || {};
      } else {
        // Appending the current object at the leaf node
        builder[obj[key]] = (builder[obj[key]] || []).concat(obj);
      }
      return builder[obj[key]];
    }, objectsByKeyValue);

    return objectsByKeyValue;
  }, {});

Example:

console.log(
  JSON.stringify(
    groupBy(["a", "b", "c"])([
      { a: "xxx", b: "yyy", c: "zzz", q: "111" },
      { a: "xxx", b: "yyy", c: "diff" },
      { a: "xxx", b: "yyy", c: "zzz", t: "222" },
    ]),
    null,
    2,
  ),
);

/*
{
  "xxx": {
    "yyy": {
      "zzz": [
        {
          "a": "xxx",
          "b": "yyy",
          "c": "zzz",
          "q": "111"
        },
        {
          "a": "xxx",
          "b": "yyy",
          "c": "zzz",
          "t": "222"
        }
      ],
      "diff": [
        {
          "a": "xxx",
          "b": "yyy",
          "c": "diff"
        }
      ]
    }
  }
}
*/

@MaccerC
Copy link

MaccerC commented Aug 5, 2020

@mikaello Very good! This code works like a charm. Thank you!

@DYW972
Copy link

DYW972 commented Sep 3, 2020

Very efficient ! Just a question it’s possible to change the key name ? Exemples mix it with a String, convert it into number (for exemple if the value is a time stamp).

@mikaello
Copy link
Author

mikaello commented Sep 4, 2020

Just a question it’s possible to change the key name ? Exemples mix it with a String, convert it into number (for exemple is the value is a time stamp).

Sure, just change how the value variable is generated:

const groupBy = keys => array =>
  array.reduce((objectsByKeyValue, obj) => {
+   const value = someCustomGroupByKeyGenerator(keys.map(key => obj[key])); // Add your custom generator for keys here
    objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
    return objectsByKeyValue;
  }, {});

The default is to just concatenate with -.

@DYW972
Copy link

DYW972 commented Sep 8, 2020

Just a question it’s possible to change the key name ? Exemples mix it with a String, convert it into number (for exemple is the value is a time stamp).

Sure, just change how the value variable is generated:

const groupBy = keys => array =>
  array.reduce((objectsByKeyValue, obj) => {
+   const value = someCustomGroupByKeyGenerator(keys.map(key => obj[key])); // Add your custom generator for keys here
    objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
    return objectsByKeyValue;
  }, {});

The default is to just concatenate with -.

Yay !! Thank you very much ! I will make something with it and come back with the result.

@shravanivish
Copy link

shravanivish commented Aug 11, 2021

hi, can you please help

const arr = [
	{ Id: "001", qty: 1 },
	{ Id: "002", qty: 2 },
	{ Id: "001", qty: 2 },
	{ Id: "003", qty: 4 },
];

const result = [];
arr.reduce(function (res, value) {
	if (!res[value.Id]) {
		res[value.Id] = { Id: value.Id, qty: 0 };
		result.push(res[value.Id]);
	}
	res[value.Id].qty += value.qty;
	return res;
}, {});
console.log(result);
// Output:
// [ 
//   { Id: '001', qty: 3 }, 
//   { Id: '002', qty: 2 }, 
//   { Id: '003', qty: 4 } 
// ]

but, I need output as

{
"001":3,
"002":2,
"003":4
}

@mikaello
Copy link
Author

mikaello commented Aug 11, 2021

@shravanivish

hi, can you please help

This does not look related to the group by function in this gist, please write unrelated questions in other places that are more appropriate, e.g. Stackoverflow.

BTW, your problem could be fixed by using another reduce, or by modifying your existing. But please follow this another place.

@shravanivish
Copy link

shravanivish commented Aug 12, 2021

@shravanivish

hi, can you please help

This does not look related to the group by function in this gist, please write unrelated questions in other places that are more appropriate, e.g. Stackoverflow.

BTW, your potsticker could be fixed by using another reduce, or by modifying your existing. But please follow this another place.

Thank You!

@melissapalmer
Copy link

melissapalmer commented Sep 21, 2021

@mikaello thanks for this, its really helpful!! Would it be possible to group nested values within an array?

For example if we had

const cars = [
  { brand: 'Audi', produced: '2016', color: 'black', drivers: [{ email: "Sebastian.Vettel@cars.com", firstName: "Sebastian", lastName: "Vettel" }, { email: "Lewis.Hamilton@cars.com", firstName: "Lewis", lastName: "Hamilton" }] },
  { brand: 'Audi', produced: '2017', color: 'white', drivers: [{ email: "Lewis.Hamilton@cars.com", firstName: "Lewis", lastName: "Hamilton" }] },
  { brand: 'Ford', produced: '2016', color: 'red', drivers: [{ email: "Lewis.Hamilton@cars.com", firstName: "Lewis", lastName: "Hamilton" }] },
  { brand: 'Ford', produced: '2016', color: 'white', drivers: [{ email: "Fernando.Alonso@cars.com", firstName: "Fernando", lastName: "Alonso" }] },
  { brand: 'Peugot', produced: '2018', color: 'white', drivers: [{ email: "Fernando.Alonso@cars.com", firstName: "Fernando", lastName: "Alonso" }, { email: "Lewis.Hamilton@cars.com", firstName: "Lewis", lastName: "Hamilton" }] }
];

Can we group by drivers.email? So result would that Lewis.Hamilton is associated with 4 cars?
eg:

{
  "Sebastian.Vettel@cars.com":
    [
      {"brand":"Audi","produced":"2016","color":"black","drivers": {"email":"Sebastian.Vettel@cars.com","firstName":"Sebastian","lastName":"Vettel"},{"email":"Lewis.Hamilton@cars.com","firstName":"Lewis","lastName":"Hamilton"}]}
    ],
  "Lewis.Hamilton@cars.com":
    [
      {"brand":"Audi","produced":"2016","color":"black","drivers":[{"email":"Sebastian.Vettel@cars.com","firstName":"Sebastian","lastName":"Vettel"},{"email":"Lewis.Hamilton@cars.com","firstName":"Lewis","lastName":"Hamilton"}]},
      {"brand":"Audi","produced":"2017","color":"white","drivers":[{"email":"Lewis.Hamilton@cars.com","firstName":"Lewis","lastName":"Hamilton"}]},
      {"brand":"Ford","produced":"2016","color":"red","drivers":[{"email":"Lewis.Hamilton@cars.com","firstName":"Lewis","lastName":"Hamilton"}]},             
      {"brand":"Peugot","produced":"2018","color":"white","drivers":[{"email":"Fernando.Alonso@cars.com","firstName":"Fernando","lastName":"Alonso"},{"email":"Lewis.Hamilton@cars.com","firstName":"Lewis","lastName":"Hamilton"}]}
    ],
  "Fernando.Alonso@cars.com":
    [
      {"brand":"Ford","produced":"2016","color":"white","drivers":[{"email":"Fernando.Alonso@cars.com","firstName":"Fernando","lastName":"Alonso"}]},      {"brand":"Peugot","produced":"2018","color":"white","drivers":[{"email":"Fernando.Alonso@cars.com","firstName":"Fernando","lastName":"Alonso"},{"email":"Lewis.Hamilton@cars.com","firstName":"Lewis","lastName":"Hamilton"}]}
    ]
}

@maciel-82
Copy link

maciel-82 commented Oct 5, 2021

is there any way to get the count which group have

Yes, that is possible. The values of a group is just an array, so you could just check the length property of that array:

const groupBy = (keys) => (array) =>
  array.reduce((objectsByKeyValue, obj) => {
    const value = keys.map((key) => obj[key]).join("-");
    objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
    return objectsByKeyValue;
  }, {});

const cars = [
  { brand: "Audi", produced: "2016", color: "black" },
  { brand: "Audi", produced: "2017", color: "white" },
  { brand: "Ford", produced: "2016", color: "red" },
  { brand: "Ford", produced: "2016", color: "white" },
  { brand: "Peugot", produced: "2018", color: "white" },
];

const groupByBrandAndYear = groupBy(["brand", "produced"]);

for (let [groupName, values] of Object.entries(groupByBrandAndYear(cars))) {
  console.log(`${groupName}: ${values.length}`);
}
# console output
Audi-2016: 1
Audi-2017: 1
Ford-2016: 2
Peugot-2018: 1

@mikaello nice work!
BTW is it possible to keep the brand and produced attributes separated in the result, instead of concatenating them?

like this:
{brand: "Audi", produced: 2016, count: 1}
{brand: "Ford", produced: 2016, count: 2}
... and so on.

Thanks

@mikaello
Copy link
Author

mikaello commented Oct 6, 2021

@maciel-82 , sure that is possible. But it may be easier to just modify the result accordingly, e.g.

const brandYearCount = Object
  .entries(groupByBrandAndYear(cars))
  .map(([, value]) =>
    ({
      brand: value[0].brand,
      produced: value[0].produced,
      count: value.length
    }))

console.log(brandYearCount)
# Output
[
 {  brand: "Audi",  count: 1,  produced: "2016"},
 {  brand: "Audi",  count: 1,  produced: "2017"},
 {  brand: "Ford",  count: 2,  produced: "2016"},
 {  brand: "Peugot",  count: 1,  produced: "2018"}
]

See JsFiddle

@maciel-82
Copy link

maciel-82 commented Oct 7, 2021

@maciel-82 , sure that is possible. But it may be easier to just modify the result accordingly, e.g.

const brandYearCount = Object
  .entries(groupByBrandAndYear(cars))
  .map(([, value]) =>
    ({
      brand: value[0].brand,
      produced: value[0].produced,
      count: value.length
    }))

console.log(brandYearCount)
# Output
[
 {  brand: "Audi",  count: 1,  produced: "2016"},
 {  brand: "Audi",  count: 1,  produced: "2017"},
 {  brand: "Ford",  count: 2,  produced: "2016"},
 {  brand: "Peugot",  count: 1,  produced: "2018"}
]

See JsFiddle

@mikaello it worked perfectly!!!
Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment