2

I have an array of objects. From this array I want to create a sorted map by the key foo. The values shall be arrays with the corresponding objects, sorted by the key bar.

// input data
const arr = [{'foo': 42, 'bar': 7}, {'foo': 1, 'bar': 2}, {'foo': 1, 'bar': 1}];

// insert magic here

// This is the result (not sure how to correctly display a map)
{
  1 -> [{'foo': 1, 'bar': 1}, {'foo': 1, 'bar': 2}],
  42 -> [{'foo': 42, 'bar': 7}]
}

It's not too difficult to find a working solution. But I'm looking for a simple and fast solution, preferrably a one-liner. How can this be done?

My current endeavors

Currently I'm stuck with this line of code. It creates a map with arrays. Is there a way to at least include the sorting functionality for the keys in there? I could then do a second step and sort the arrays separately.

const result = new Map(arr.map(i => [i.foo, i]));
KorbenDose
  • 797
  • 1
  • 7
  • 23
  • 1
    If you have a solution that isn't causing you performance problems you should go with that - otherwise this question is a little off-topic for SO. If you're having problems with the code you have you should add it to your question. – Andy Aug 19 '23 at 14:30
  • 1
    This would be a good use case for [`Array#groupToMap()`](https://github.com/tc39/proposal-array-grouping/tree/7d84f9dd9bd168ad7ef6b73f169d2df7abe01a12) if it was accepted: `arr.groupToMap(element => element.foo)`. – InSync Aug 19 '23 at 14:34
  • Does this answer your question? [Most efficient method to groupby on an array of objects](https://stackoverflow.com/questions/14446511/most-efficient-method-to-groupby-on-an-array-of-objects) – derpirscher Aug 19 '23 at 14:43
  • @derpirscher Not completely, but it's an interesting read and I may also have a use case for that. Thanks. – KorbenDose Aug 19 '23 at 14:45
  • Just for completeness, due to [some compatibility issues](https://github.com/tc39/proposal-array-grouping/issues/37), [the current proposal](https://github.com/tc39/proposal-array-grouping/tree/3a31a5c0b37929bc1a8fd775028f94cde3103ca9) is about static methods: `Map.groupBy(arr, element => element.foo)`. – InSync Aug 19 '23 at 15:00

2 Answers2

3

Step by step:

const arr = [{'foo': 42, 'bar': 7}, {'foo': 1, 'bar': 2}, {'foo': 1, 'bar': 1}];

// Map by foo:

const result = arr.reduce((acc, cur) => {
  if (!acc[cur.foo]) acc[cur.foo] = []
  
  acc[cur.foo].push(cur)
  
  return acc;
}, {})

// Sort by bar:

Object.values(result).forEach((arr) => arr.sort((a, b) => a.bar - b.bar))

// Final result:

console.log(result)

You can also make it a one-liner and still use built-in methods, but performance would be worse as this is creating intermediate objects and arrays and sorting the arrays continuously (which is totally unnecessary):

const arr = [{'foo': 42, 'bar': 7}, {'foo': 1, 'bar': 2}, {'foo': 1, 'bar': 1}];
const result = arr.reduce((acc, cur) => ({ ...acc, [cur.foo]: [...(acc[cur.foo] || []), cur].sort((a, b) => a.bar - b.bar) }), {})

console.log(result)

If you need the keys of the map to be sorted, I recommend you use an actual Map instead of using a regular Object. This is what the MDN Map docs say about this:

You might also want to take a look at this other question (pay attention to the date of the answers): Does ES6 introduce a well-defined order of enumeration for object properties?

Although the keys of an ordinary Object are ordered now, this was not always the case, and the order is complex. As a result, it's best not to rely on property order.

The keys in Map are ordered in a simple, straightforward way: A Map object iterates entries, keys, and values in the order of entry insertion.

Therefore, you just need to sort the array before using reduce, and replace the initialValue param with new Map():

const arr = [{'foo': 42, 'bar': 7}, {'foo': 1, 'bar': 2}, {'foo': 1, 'bar': 1}];

// Map by foo:

const result = arr.sort((a, b) => a.foo - b.foo).reduce((acc, cur) => {
  const arr = acc.get(cur.foo)
  
  acc.set(cur.foo, arr ? [...arr, cur] : [cur])
  
  return acc;
}, new Map())

// Sort by bar:

for (const [_, arr] of result) {
  arr.sort((a, b) => a.bar - b.bar)
}

// Final result:

console.log(Object.fromEntries(result.entries()))

Just keep in mind the keys in the Map are ordered by insertion order, so if you have keys A, B and C and add D later, D will be the last one (4th) not the first.

Danziger
  • 19,628
  • 4
  • 53
  • 83
  • Thanks, this looks good. Which part of the code guarantees that the keys for the map are in sorted order? – KorbenDose Aug 19 '23 at 14:43
  • @KorbenDose None, because maps don't have any natural order ... – derpirscher Aug 19 '23 at 14:44
  • 2
    @KorbenDose Numeric keys (which are greater than or equal to 0 and less than (2^32)-1, ie: valid array keys) will automatically be iterated in increasing order (that's only guaranteed if your engine supports ECMAScript 2020, which most modern browsers do. Older engines tended to sort like this anyway as well) (but if you want to be explicit about it, it's best to use a `Map`) – Nick Parsons Aug 19 '23 at 14:46
  • @KorbenDose None, I'm only sorting the objects by `bar`. You can sort the array before using `.reduce()`, but I recommend your read this: https://stackoverflow.com/questions/30076219/does-es6-introduce-a-well-defined-order-of-enumeration-for-object-properties – Danziger Aug 19 '23 at 14:48
  • @derpirscher In the [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) it says that the Map remembers the original insertion order of the keys. I understand that in this solution, we're not actually using a Map, so I guess this is reason why it works. – KorbenDose Aug 19 '23 at 14:49
  • @KorbenDose yes, but that's not a natural order (ie not based on the values of the keys). Thus, when you have a map with keys 3, 5, 7 which you inserted in that order, the map will be traversed in that order. But if you add a key of 4 later on, it will always be after 3, 5, 7 ... – derpirscher Aug 19 '23 at 14:51
  • @derpirscher Since ES2020, the spec says that object keys will be iterated in increasing order (even if 4 is added later), as 4 is a valid numeric array key (ie: between 0 and 2^32-1, [src](https://262.ecma-international.org/14.0/#sec-ordinaryownpropertykeys)) (but I still think a `Map` is better to use if order is needed) – Nick Parsons Aug 19 '23 at 14:54
  • @derpirscher I understand that, thanks. I will only create the Map once and will not add keys at a later stage. Also in advance I want to chose if I want to order in ascending or descending order. This is why I'm specifically looking for the sorting functionality. – KorbenDose Aug 19 '23 at 14:54
  • 1
    @KorbenDose Check the updated answer, the alternative with `Map` is probably what you are after. – Danziger Aug 19 '23 at 15:00
  • 1
    @Danziger Perfect, thanks a lot! – KorbenDose Aug 19 '23 at 15:06
1

A fast one-liner:

const arr = [{'foo': 42, 'bar': 7}, {'foo': 1, 'bar': 2}, {'foo': 1, 'bar': 1}];

const result = arr.toSorted((a, b) => a.bar - b.bar).reduce((r, item) => (r[item.foo] ??= []).push(item) && r, {});

console.log(result)
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17