Skip to content

Instantly share code, notes, and snippets.

@JamieMason
Created September 14, 2018 07:38
Show Gist options
  • Save JamieMason/0566f8412af9fe6a1d470aa1e089a752 to your computer and use it in GitHub Desktop.
Save JamieMason/0566f8412af9fe6a1d470aa1e089a752 to your computer and use it in GitHub Desktop.
Group Array of JavaScript Objects by Key or Property Value

Group Array of JavaScript Objects by Key or Property Value

Implementation

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

Or using an implicit return (slower):

const groupBy = key => array =>
  array.reduce(
    (objectsByKeyValue, obj) => ({
      ...objectsByKeyValue,
      [obj[key]]: (objectsByKeyValue[obj[key]] || []).concat(obj)
    }),
    {}
  );

Usage

const cars = [
  { brand: 'Audi', color: 'black' },
  { brand: 'Audi', color: 'white' },
  { brand: 'Ferarri', color: 'red' },
  { brand: 'Ford', color: 'white' },
  { brand: 'Peugot', color: 'white' }
];

const groupByBrand = groupBy('brand');
const groupByColor = groupBy('color');

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

Output

{
  "carsByBrand": {
    "Audi": [
      {
        "brand": "Audi",
        "color": "black"
      },
      {
        "brand": "Audi",
        "color": "white"
      }
    ],
    "Ferarri": [
      {
        "brand": "Ferarri",
        "color": "red"
      }
    ],
    "Ford": [
      {
        "brand": "Ford",
        "color": "white"
      }
    ],
    "Peugot": [
      {
        "brand": "Peugot",
        "color": "white"
      }
    ]
  },
  "carsByColor": {
    "black": [
      {
        "brand": "Audi",
        "color": "black"
      }
    ],
    "white": [
      {
        "brand": "Audi",
        "color": "white"
      },
      {
        "brand": "Ford",
        "color": "white"
      },
      {
        "brand": "Peugot",
        "color": "white"
      }
    ],
    "red": [
      {
        "brand": "Ferarri",
        "color": "red"
      }
    ]
  }
}
@hspeight
Copy link

Worked perfectly for me and very easy to implement.
Much appreciated.
Thanks!

@Sanyug92
Copy link

Sanyug92 commented Oct 6, 2019

Works perfectly! Thanks!

@edison12a
Copy link

Thanks!

@nkhil
Copy link

nkhil commented Oct 28, 2019

Thanks so much @JamieMason !

@daraxdray
Copy link

Thank you greatly....

@SnehaAitha
Copy link

SnehaAitha commented Dec 19, 2019

I am trying to use the group by function on a JSON array using the inner JSON value as a key as shown below. But unable to read the inner JSON value. Here is my JSON array.

Code Snippet
NotificationData = [ 
   { 
      "eventId":"90989",
      "eventTime":"2019-12-11T11:20:53+04:00",
      "eventType":"yyyy",
      "event":{ 
         "ServiceOrder":{ 
            "externalId":"2434",
            "priority":"1"
         }
      }
   },
   { 
      "eventId":"6576",
      "eventTime":"2019-12-11T11:20:53+04:00",
      "eventType":"yyyy",
      "event":{ 
         "ServiceOrder":{ 
            "externalId":"78657",
            "priority":"1"
         }
      }
   }
]

// GroupBy Logic used:

   const groupBy = (array, key) => {
      return array.reduce((result, currentValue) => {
        (result[currentValue[key]] = result[currentValue[key]] || []).push(
          currentValue
        );
        return result;
      }, {});
    };

    const serviceOrdersGroupedByExternalId = groupBy(this.NotificationData, 'event.ServiceOrder.externalId');//this line of code is not working as it is unable to locate the external id value.

*****************************************************************************************************************************************************

Desired Output:

{ 
   "2434":[ 
      { 
         "eventId":"90989",
         "eventTime":"2019-12-11T11:20:53+04:00",
         "eventType":"yyyy",
         "event":{ 
            "ServiceOrder":{ 
               "priority":"1"
            }
         }
      }
   ],
   "78657":[ 
      { 
         "eventId":"6576",
         "eventTime":"2019-12-11T11:20:53+04:00",
         "eventType":"yyyy",
         "event":{ 
            "ServiceOrder":{ 
               "priority":"1"
            }
         }
      }
   ]
}

Kindly suggest me a way to do this.

@senthil-fullstack
Copy link

senthil-fullstack commented Dec 19, 2019

@SnehaAitha, here is my suggestion.

Code Snippet
  // GroupBy Logic:
  const groupBy = (array, key) => {
    return array.reduce((result, currentValue) => {
      // get the nested propert value
      const objKey = nestedObjectByString(currentValue, key);
      result[objKey] = (result[objKey] || []).concat(
        currentValue)
      return result;
    }, {});
  };

  // return value of nested property of an object
  const nestedObjectByString = (obj, key) => {
    key = key.replace(/\[(\w+)\]/g, '.$1');  // convert indexes to properties
    key = key.replace(/^\./, ''); // strip a leading dot
    const a = key.split('.');
    for (let i = 0, n = a.length; i < n; ++i) {
      const k = a[i];
      if (k in obj) {
        obj = obj[k];
      } else {
        return;
      }
    }
    return obj;
  }

const NotificationData = [
      {
        "eventId": "90989",
        "eventTime": "2019-12-11T11:20:53+04:00",
        "eventType": "yyyy",
        "event": {
          "ServiceOrder": {
            "externalId": "2434",
            "priority": "1"
          }
        }
      },
      {
        "eventId": "6576",
        "eventTime": "2019-12-11T11:20:53+04:00",
        "eventType": "yyyy",
        "event": {
          "ServiceOrder": {
            "externalId": "78657",
            "priority": "1"
          }
        }
      }
    ];

const serviceOrdersGroupedByExternalId = groupBy(NotificationData, 'event.ServiceOrder.externalId');

Let me know if it doesn't work.

@dcrabbeYapily
Copy link

this is very close to what I want but I have real issues with grouping by a nested value.

ie

[ {
          title: 'Sains',
          amount: 100,
          category: 'shopping',

          enrichment: {
            categorisation: {
              categories: ['SHOPPING', 'GROCERIES'],
              source: 'MODEL',
            },
          },
          date: '2021-01-31',
        },

        {
          title: 'Tesco',
          amount: 66,
          category: 'shopping',
          enrichment: {
            categorisation: {
              categories: ['SHOPPING', 'GROCERIES'],
              source: 'MODEL',
            },
          },
          date: '2021-01-31',
        }]

This works if I group from the top level 'category'. But really I need to group (and sum) by cont categorisation

const sums = [
        ...arr
          .reduce((map: any, item: any) => {
            const categorisation = item?.enrichment?.categorisation?.categories; // I want to use this one
            const { category: key, amount } = item; // something needs to change here 
            const prev = map.get(key);
            if (prev) {
              prev.amount += amount;
            } else {
              map.set(
                key,
                Object.assign(
                  {},
                  { category: categorisation[0], categoryTitle: item.title, amount: item.amount }
                )
              );
            }
            return map;
          }, new Map())
          .values(),
      ];
      return sums;

any pointers appreciated.

D.

@JamieMason
Copy link
Author

@jsommr
Copy link

jsommr commented Mar 19, 2021

Updated version for TS 4.2.3

function groupBy<T extends Record<string, any>, K extends keyof T>(
  array: T[],
  key: K | { (obj: T): string }
): Record<string, T[]> {
  const keyFn = key instanceof Function ? key : (obj: T) => obj[key];
  return array.reduce((objectsByKeyValue, obj) => {
    const value = keyFn(obj);
    objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
    return objectsByKeyValue;
  }, {} as Record<string, T[]>);
}

let arr = [
  { test: "abc", a: 1, color: { value: "black" } },
  { test: "abc", a: 2, color: { value: "black" } },
  { test: "def", a: 1, color: { value: "white" } },
];

let result = groupBy(arr, (obj) => obj.color.value);

console.log(JSON.stringify(result, null, 2));

@JamieMason
Copy link
Author

Nice @nerfpops 👍

@humeyra92
Copy link

nice!

@r-souza
Copy link

r-souza commented Apr 5, 2021

Nice!!!

@VictorLM
Copy link

Save my day. Thanks.

@viniciuSquare
Copy link

Thanks for that solution, saved me a lot o time.

@hussainahmad
Copy link

@JamieMason Thanks for the great code but if we want to get like this

group data = [{"title":"AB", data:[{},{},{}]}, {"title":"BC", data:[{},{},{}]}]

@JamieMason
Copy link
Author

Sorry @hussainahmad, I don't understand?

@hussainahmad
Copy link

@JamieMason I am talking to convert array like this

[
  {
    "title": "Audi",
    "data": [
      {
        "brand": "Audi",
        "color": "black"
      },
      {
        "brand": "Audi",
        "color": "white"
      }
    ]
  },
  {
    "title": "Ferarri",
    "data": [
      {
        "brand": "Ferarri",
        "color": "red"
      }
    ]
  },
  {
    "title": "Ford",
    "data": [
      {
        "brand": "Ford",
        "color": "white"
      }
    ]
  },
  {
    "title": "Peugot",
    "data": [
      {
        "brand": "Peugot",
        "color": "white"
      }
    ]
  }
]

@JamieMason
Copy link
Author

You want to convert that into [{"title":"AB", data:[{},{},{}]}, {"title":"BC", data:[{},{},{}]}]? can you give some more context on what you're trying to build? why would you want those empty objects for example?

@hussainahmad
Copy link

@JamieMason empty objects was just an example sorry for confusing you. , let assume I have a data

const cars = [
  { brand: 'Audi', color: 'black' },
  { brand: 'Audi', color: 'white' },
  { brand: 'Ferarri', color: 'red' },
  { brand: 'Ford', color: 'white' },
  { brand: 'Peugot', color: 'white' }
];

and I want to convert this array in to this array

[
  {
    "title": "Audi",
    "data": [
      {
        "brand": "Audi",
        "color": "black"
      },
      {
        "brand": "Audi",
        "color": "white"
      }
    ]
  },
  {
    "title": "Ferarri",
    "data": [
      {
        "brand": "Ferarri",
        "color": "red"
      }
    ]
  },
  {
    "title": "Ford",
    "data": [
      {
        "brand": "Ford",
        "color": "white"
      }
    ]
  },
  {
    "title": "Peugot",
    "data": [
      {
        "brand": "Peugot",
        "color": "white"
      }
    ]
  }
]

@JamieMason
Copy link
Author

I think this should do it @hussainahmad:

const groupByBrand = groupBy('brand');
const carsByBrand = groupByBrand(cars);
const carBrands = Object.entries(carsByBrand).map(([title, data]) => ({ title, data }));

@hussainahmad
Copy link

Really Thanks to you @JamieMason , May God bless you

@JamieMason
Copy link
Author

You're welcome

@MadhuriTank
Copy link

@JamieMason, Thanks a lot!!! It is very useful !!!!

@scelloo-elegeonyejerome
Copy link

@JamieMason Amazing work.. Quick One, how can one get the count of each group?

@JamieMason
Copy link
Author

Like this @scelloo-elegeonyejerome:

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

const cars = [
  { brand: 'Audi', color: 'black' },
  { brand: 'Audi', color: 'white' },
  { brand: 'Ferarri', color: 'red' },
  { brand: 'Ford', color: 'white' },
  { brand: 'Peugot', color: 'white' },
];

const groupByBrand = groupBy('brand');
const carsByBrand = groupByBrand(cars);

Object.entries(carsByBrand).forEach(([brand, cars]) => {
  console.log(`${cars.length} ${brand}`);
});

Outputs

"2 Audi"
"1 Ferarri"
"1 Ford"
"1 Peugot"

@chgad
Copy link

chgad commented Aug 29, 2022

@JamieMason arriving late to the party and newbie to JS: Is there a particular reason for double arrow-functions here or can it be rewritten as function with two parameters key, array ?

@JamieMason
Copy link
Author

You could do this @chgad, you're right.

- const groupBy = (key) => (array) =>
+ const groupBy = (key, array) =>

The reason for double arrow-functions (a function creator / a function which returns another function) is so we can do things like this:

export const groupByBrand = groupBy('brand');

so users of your module do this:

groupByBrand(cars);

Instead of this:

groupBy('brand', cars);

Broadly speaking it's a matter of style or taste. It comes from a functional programming-like style of writing JS.

Really – the benefits of it aren't obvious or arguably even applicable in this little example, but generating functions can make for really reusable, readable code. You need to be careful though and develop a sense for knowing when doing this would be an upgrade and not a downgrade.

A rule of thumb would be to not do what I'm doing here, unless you're confident in how to generate and compose functions in a functional style.

@aboudrea
Copy link

aboudrea commented Aug 30, 2023

I think this should do it @hussainahmad:

const groupByBrand = groupBy('brand');
const carsByBrand = groupByBrand(cars);
const carBrands = Object.entries(carsByBrand).map(([title, data]) => ({ title, data }));

@JamieMason how could I add another value to the top collection, if I had engine as a value in the original array:

{
  "title": "Audi",
  "engine": "V8",
  "data": [
    {
      "brand": "Audi",
      "color": "black"
    },
    {
      "brand": "Audi",
      "color": "white"
    }
  ]
}

@JamieMason
Copy link
Author

@aboudrea please add more detail

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