172

Is it possible to sort the entries of a es6 map object?

var map = new Map();
map.set('2-1', foo);
map.set('0-1', bar);

results in:

map.entries = {
    0: {"2-1", foo },
    1: {"0-1", bar }
}

Is it possible to sort the entries based on their keys?

map.entries = {
    0: {"0-1", bar },
    1: {"2-1", foo }
}
Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Ivan Bacher
  • 5,855
  • 9
  • 36
  • 56

14 Answers14

207

According MDN documentation:

A Map object iterates its elements in insertion order.

You could do it this way:

var map = new Map();
map.set('2-1', "foo");
map.set('0-1', "bar");
map.set('3-1', "baz");

var mapAsc = new Map([...map.entries()].sort());

console.log(mapAsc)

Using .sort(), remember that the array is sorted according to each character's Unicode code point value, according to the string conversion of each element. So 2-1, 0-1, 3-1 will be sorted correctly.

  • 11
    you can shorten that function(a,b) stuffs to: `var mapAsc = new Map([...map.entries()].sort((a,b) => a[0] > b[0]));` using arrow function (lambda) – Jimmy Chandra Jul 01 '15 at 11:02
  • 2
    In fact, it acutally lexically compares `2-1,foo` to `0-1,bar` and `3-1,baz` – Bergi Jul 01 '15 at 17:23
  • 31
    @JimmyChandra: [Don't use `(a,b) => a[0] > b[0]`!](http://stackoverflow.com/q/24080785/1048572) – Bergi Jul 01 '15 at 17:25
  • @Bergi yes, the correct usage is: `var mapAscByKey = new Map([...map.entries()].sort((a,b) => a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0));`. As I said taking the order according to the value converted into a string. In this case if the keys are unique there will be no problem to order it that way. – Walter Chapilliquen - wZVanG Jul 01 '15 at 23:13
  • I'm using Babel with es2015-loose and stage-1. I had to also use the babel-plugin-transform-es2015-spread in order to get this working. – Dave Jensen Feb 01 '17 at 01:17
  • 5
    The `...` dots are important otherwise you are trying to sort a MapIterator – muttonUp Apr 10 '17 at 12:22
  • 1
    The MDN doc actually states: "It should be noted that a Map which is a map of an object, especially a dictionary of dictionaries, will only map to the object's insertion order—which is random and not ordered." - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map – Johann Jun 18 '17 at 09:02
  • 2
    If the map keys are numbers, you will get invalid results with numbers like `1e-9` being put after `100` in sorted map. Code that works with numbers: `new Map([...map.entries()].sort((e1, e2) => e1[0] - e2[0]))` – cdalxndr Jan 07 '19 at 12:59
  • Unfortunately I get a GulpUglifyError because of the `...`. Is there another way? – laprof Oct 09 '19 at 17:24
  • @laprof a Gulp error is not a JS error, why a vote down? :(. Maybe in the Gulp documentation you will find a topic related about the spread operator – Walter Chapilliquen - wZVanG Oct 28 '19 at 03:27
  • 1
    This only logs `{}` to the console for me when I run the Stack Snippet (Chrome). – Laurel Jun 02 '20 at 00:30
  • If you're going to be sorting all the time, may as well stick to an Array instead of converting from a Map to an Array to a Map. That has CPU cost. – trusktr Oct 07 '20 at 22:23
150

Short answer

 new Map([...map].sort((a, b) => 
   // Some sort function comparing keys with a[0] b[0] or values with a[1] b[1]
 ))

If you're expecting strings: As normal for .sort you need to return -1 if lower and 0 if equal; for strings, the recommended way is using .localeCompare() which does this correctly and automatically handles awkward characters like ä where the position varies by user locale.

So here's a simple way to sort a map by string keys:

 new Map([...map].sort((a, b) => String(a[0]).localeCompare(b[0])))

...and by string values:

 new Map([...map].sort((a, b) => String(a[1]).localeCompare(b[1])))

These are type-safe in that they won't throw an error if they hit a non-string key or value. The String() at the start forces a to be a string (and is good for readability), and .localeCompare() itself forces its argument to be a string without hitting an error.


In detail with examples

tldr: ...map.entries() is redundant, just ...map is fine; and a lazy .sort() without passing a sort function risks weird edge case bugs caused by string coercion.

The .entries() in [...map.entries()] (suggested in many answers) is redundant, probably adding an extra iteration of the map unless the JS engine optimises that away for you.

In the simple test case, you can do what the question asks for with:

new Map([...map].sort())

...which, if the keys are all strings, compares squashed and coerced comma-joined key-value strings like '2-1,foo' and '0-1,[object Object]', returning a new Map with the new insertion order:

Note: if you see only {} in SO's console output, look in your real browser console

const map = new Map([
  ['2-1', 'foo'],
  ['0-1', { bar: 'bar' }],
  ['3-5', () => 'fuz'],
  ['3-2', [ 'baz' ]]
])

console.log(new Map([...map].sort()))

HOWEVER, it's not a good practice to rely on coercion and stringification like this. You can get surprises like:

const map = new Map([
  ['2', '3,buh?'],
  ['2,1', 'foo'],
  ['0,1', { bar: 'bar' }],
  ['3,5', () => 'fuz'],
  ['3,2', [ 'baz' ]],
])

// Compares '2,3,buh?' with '2,1,foo'
// Therefore sorts ['2', '3,buh?'] ******AFTER****** ['2,1', 'foo']
console.log('Buh?', new Map([...map].sort()))

// Let's see exactly what each iteration is using as its comparator
for (const iteration of map) {
  console.log(iteration.toString())
}

Bugs like this are really hard to debug - don't risk it!

If you want to sort on keys or values, it's best to access them explicitly with a[0] and b[0] in the sort function, like above; or with array destructuring in the function arguments:

const map = new Map([
  ['2,1', 'this is overwritten'],
  ['2,1', '0,1'],
  ['0,1', '2,1'],
  ['2,2', '3,5'],
  ['3,5', '2,1'],
  ['2', ',9,9']
])

// Examples using array destructuring. We're saying 'keys' and 'values'
// in the function names so it's clear and readable what the intent is. 
const sortStringKeys = ([a], [b]) => String(a).localeCompare(b)
const sortStringValues = ([,a], [,b]) => String(a).localeCompare(b)

console.log('By keys:', new Map([...map].sort(sortStringKeys)))
console.log('By values:', new Map([...map].sort(sortStringValues)))

And if you need a different comparison than alphabetical order of strings, don't forget to always make sure you return -1 and 1 for before and after, not false or 0 as with raw a[0] > b[0] because that is treated as equals.

user56reinstatemonica8
  • 32,576
  • 21
  • 101
  • 125
  • 23
    What an answer, we gotta fix S.O. discovery mechanisms. Something this good shouldn't be lost down here. – serraosays Sep 18 '18 at 18:42
  • Just wanted to add for whom wants to sort this kind of map: Map console.log('By keys:', new Map([...your_map].sort(([a]:[string, string[]], [b]:[string, string[]]) => String(a[0]).localeCompare(b[0])))) console.log('By values:', new Map([...your_map].sort(([,a]:[string, string[]], [,b]:[string, string[]]) => String(a[0]).localeCompare(b[0])))) – Jonah Tornovsky Apr 25 '22 at 09:48
  • @serraosays yeah that happens when a good answer shows up after the accepted answer, no way to force the asker to reconsider which answer is right – jcollum Jun 13 '23 at 17:11
38

Convert Map to an array using Array.from, sort array, convert back to Map, e.g.

new Map(
  Array
    .from(eventsByDate)
    .sort((a, b) => {
      // a[0], b[0] is the key of the map
      return a[0] - b[0];
    })
)
Gajus
  • 69,002
  • 70
  • 275
  • 438
8

I would suggest to use a custom iterator for your map object to achieve a sorted access, like so:

map[Symbol.iterator] = function* () {
    yield* [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]));
}

Using an iterator has the advantage of that it has only to be declared once. After adding/deleting entries in the map, a new for-loop over the map would automatically reflect this changes using an iterator. Sorted copies as shown in most of the above answers would not as they only reflect the map's state at exactly one point in time.

Here's the complete working example using your initial situation.

var map = new Map();
map.set('2-1', { name: 'foo' });
map.set('0-1', { name: 'bar' });

for (let [key, val] of map) {
    console.log(key + ' - ' + val.name);
}
// 2-1 - foo
// 1-0 - bar

map[Symbol.iterator] = function* () {
    yield* [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]));
}

for (let [key, val] of map) {
    console.log(key + ' - ' + val.name);
}
// 1-0 - bar
// 2-1 - foo

map.set('2-0', { name: 'zzz' });

for (let [key, val] of map) {
    console.log(key + ' - ' + val.name);
}
// 1-0 - bar
// 2-0 - zzz
// 2-1 - foo

Regards.

Sebastian
  • 131
  • 1
  • 5
  • 1
    Interesting answer. It's worth mentioning that the map will only sort its output on iterations that are _directly_ on the map itself, like `[...map]` and `for (… of map)`. It won't sort iterations of `map.keys()`, `map.entries()` or `map.values()`. For example, someone might expect `[...map.values()]` to have the same output as `[...map].map(([,value]) => value)`, but with this applied, the former would have insertion order, while the latter would apply this sort. – user56reinstatemonica8 Feb 21 '23 at 11:19
6

The idea is to extract the keys of your map into an array. Sort this array. Then iterate over this sorted array, get its value pair from the unsorted map and put them into a new map. The new map will be in sorted order. The code below is it's implementation:

var unsortedMap = new Map();
unsortedMap.set('2-1', 'foo');
unsortedMap.set('0-1', 'bar');

// Initialize your keys array
var keys = [];
// Initialize your sorted maps object
var sortedMap = new Map();

// Put keys in Array
unsortedMap.forEach(function callback(value, key, map) {
    keys.push(key);
});

// Sort keys array and go through them to put in and put them in sorted map
keys.sort().map(function(key) {
    sortedMap.set(key, unsortedMap.get(key));
});

// View your sorted map
console.log(sortedMap);
Mikematic
  • 329
  • 3
  • 7
  • 2
    The unsorted keys can be determined simply by using `unsortedMap.keys()`. Also `keys.sort().map...` should be `keys.sort().forEach...`. – faintsignal Feb 25 '17 at 17:33
5

You can convert to an array and call array soring methods on it:

[...map].sort(/* etc */);
wesbos
  • 25,839
  • 30
  • 106
  • 143
4

2 hours spent to get into details.

Note that the answer for question is already given at https://stackoverflow.com/a/31159284/984471

However, the question has keys that are not usual ones,
A clear & general example with explanation, is below that provides some more clarity:

.

let m1 = new Map();

m1.set(6,1); // key 6 is number and type is preserved (can be strings too)
m1.set(10,1);
m1.set(100,1);
m1.set(1,1);
console.log(m1);

// "string" sorted (even if keys are numbers) - default behaviour
let m2 = new Map( [...m1].sort() );
//      ...is destructuring into individual elements
//      then [] will catch elements in an array
//      then sort() sorts the array
//      since Map can take array as parameter to its constructor, a new Map is created
console.log('m2', m2);

// number sorted
let m3 = new Map([...m1].sort((a, b) => {
  if (a[0] > b[0]) return 1;
  if (a[0] == b[0]) return 0;
  if (a[0] < b[0]) return -1;
}));
console.log('m3', m3);

// Output
//    Map { 6 => 1, 10 => 1, 100 => 1, 1 => 1 }
// m2 Map { 1 => 1, 10 => 1, 100 => 1, 6 => 1 }
//           Note:  1,10,100,6  sorted as strings, default.
//           Note:  if the keys were string the sort behavior will be same as this
// m3 Map { 1 => 1, 6 => 1, 10 => 1, 100 => 1 }
//           Note:  1,6,10,100  sorted as number, looks correct for number keys

Hope that helps.

Manohar Reddy Poreddy
  • 25,399
  • 9
  • 157
  • 140
3

Unfortunately, not really implemented in ES6. You have this feature with OrderedMap.sort() from ImmutableJS or _.sortBy() from Lodash.

devside
  • 2,171
  • 1
  • 19
  • 23
3

One way is to get the entries array, sort it, and then create a new Map with the sorted array:

let ar = [...myMap.entries()];
sortedArray = ar.sort();
sortedMap = new Map(sortedArray);

But if you don't want to create a new object, but to work on the same one, you can do something like this:

// Get an array of the keys and sort them
let keys = [...myMap.keys()];
sortedKeys = keys.sort();

sortedKeys.forEach((key)=>{
  // Delete the element and set it again at the end
  const value = this.get(key);
  this.delete(key);
  this.set(key,value);
})
Moshe Kohavi
  • 387
  • 3
  • 8
2

The snippet below sorts given map by its keys and maps the keys to key-value objects again. I used localeCompare function since my map was string->string object map.

var hash = {'x': 'xx', 't': 'tt', 'y': 'yy'};
Object.keys(hash).sort((a, b) => a.localeCompare(b)).map(function (i) {
            var o = {};
            o[i] = hash[i];
            return o;
        });

result: [{t:'tt'}, {x:'xx'}, {y: 'yy'}];

Abdullah Gürsu
  • 3,010
  • 2
  • 17
  • 12
0

Perhaps a more realistic example about not sorting a Map object but preparing the sorting up front before doing the Map. The syntax gets actually pretty compact if you do it like this. You can apply the sorting before the map function like this, with a sort function before map (Example from a React app I am working on using JSX syntax)

Mark that I here define a sorting function inside using an arrow function that returns -1 if it is smaller and 0 otherwise sorted on a property of the Javascript objects in the array I get from an API.

report.ProcedureCodes.sort((a, b) => a.NumericalOrder < b.NumericalOrder ? -1 : 0).map((item, i) =>
                        <TableRow key={i}>

                            <TableCell>{item.Code}</TableCell>
                            <TableCell>{item.Text}</TableCell>
                            {/* <TableCell>{item.NumericalOrder}</TableCell> */}
                        </TableRow>
                    )
Tore Aurstad
  • 3,189
  • 1
  • 27
  • 22
0

As far as I see it's currently not possible to sort a Map properly.

The other solutions where the Map is converted into an array and sorted this way have the following bug:

var a = new Map([[1, 2], [3,4]])
console.log(a);    // a = Map(2) {1 => 2, 3 => 4}

var b = a;
console.log(b);    // b = Map(2) {1 => 2, 3 => 4}

a = new Map();     // this is when the sorting happens
console.log(a, b); // a = Map(0) {}     b = Map(2) {1 => 2, 3 => 4}

The sorting creates a new object and all other pointers to the unsorted object get broken.

LachoTomov
  • 3,312
  • 30
  • 42
0

Slight variation - I didn't have spread syntax and I wanted to work on an object instead of a Map.

Object.fromEntries(Object.entries(apis).sort())
Nick Grealy
  • 24,216
  • 9
  • 104
  • 119
0

Here is function that sort Map() by decreasing.

function groupBy(list, keyGetter) {
    const map = new Map();
    list.forEach((item) => {
        const key = keyGetter(item);
        const collection = map.get(key);
        if (!collection) {
            map.set(key, [item]);
        } else {
            collection.push(item);
        }
    });

    const sortedMap = new Map();
    [...map].sort((a, b) => b[1].length - a[1].length).forEach(e => sortedMap.set(e[0], e[1]));

    return sortedMap;
}
const test = groupBy(array, item => item.fieldName);