32

I want to do something like this:

var data = [
    {
        sortData: {a: 'a', b: 2}
    },
    {
        sortData: {a: 'a', b: 1}
    },
    {
        sortData: {a: 'b', b: 5}
    },
    {
        sortData: {a: 'a', b: 3}
    }
];

data = _.sortBy(data, ["sortData.a", "sortData.b"]);

_.map(data, function(element) {console.log(element.sortData.a + " " + element.sortData.b);});

And have it output this:

"a 1"
"a 2"
"a 3"
"b 5"

Unfortunately, this doesn't work and the array remains sorted in its original form. This would work if the fields weren't nested inside the sortData. How can I use lodash/underscore to sort an array of objects by more than one nested field?

I've turned this into a lodash feature request: https://github.com/lodash/lodash/issues/581

Community
  • 1
  • 1
Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356

8 Answers8

44

Update: See the comments below, this is not a good solution in most cases.


Someone kindly answered in the issue I created. Here's his answer, inlined:

_.sortBy(data, function(item) {
   return [item.sortData.a, item.sortData.b];
});

I didn't realize that you're allowed to return an array from that function. The documentation doesn't mention that.

Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356
  • 18
    This is because when you return an array it is converted to string and then compared, but if your second item is number and you want to sort by that second value as number, it would not work. – Tomas Kirda Jan 14 '15 at 20:48
  • Tomas called out an important edge case. Use _.orderBy as suggested by kenjiru, or _.sortBy with the correct syntax a la Tomas – dwu39 Jul 28 '22 at 21:27
42

If you need to specify the sort direction, you can use _.orderBy with the array of functions syntax from Lodash 4.x:

_.orderBy(data, [
  function (item) { return item.sortData.a; },
  function (item) { return item.sortData.b; }
], ["asc", "desc"]);

This will sort first ascending by property a, and for objects that have the same value for property a, will sort them descending by property b.

It works as expected when the a and b properties have different types.

Here is a jsbin example using this syntax.

kenjiru
  • 780
  • 6
  • 11
22

There is a _.sortByAll method in lodash version 3:

https://github.com/lodash/lodash/blob/3.10.1/doc/README.md#_sortbyallcollection-iteratees

Lodash version 4, it has been unified:

https://lodash.com/docs#sortBy

Other option would be to sort values yourself:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

function compareValues(v1, v2) {
    return (v1 > v2) 
        ? 1 
        : (v1 < v2 ? -1 : 0);
};


var data = [
    { a: 2, b: 1 },
    { a: 2, b: 2 },
    { a: 1, b: 3 }
];

data.sort(function (x, y) {
    var result = compareValues(x.a, y.a);

    return result === 0 
        ? compareValues(x.b, y.b) 
        : result;
});

// data after sort:
// [
//     { a: 1, b: 3 },
//     { a: 2, b: 1 },
//     { a: 2, b: 2 }
// ];
Aurelio
  • 24,702
  • 9
  • 60
  • 63
Tomas Kirda
  • 8,347
  • 3
  • 31
  • 23
14

The awesome, simple way is:

_.sortBy(data, [function(item) {
    return item.sortData.a;
}, function(item) {
    return item.sortData.b;
}]);

I found it from check the source code of lodash, it always check the function one by one.

Hope that help.

KimKha
  • 4,370
  • 1
  • 37
  • 45
  • 3
    Also, if you want a "reverse" sort for either of those sorting functions, just add a "-" to the value being returned. `return -item.sortData.a;` for example. – mattrowsboats Jan 19 '17 at 21:49
11

With ES6 easy syntax and lodash

sortBy(item.sortData, (item) => (-item.a), (item) => (-item.b))
Cem Arguvanlı
  • 643
  • 10
  • 15
0

I think this could work in most cases with underscore:

var properties = ["sortData.a", "sortData.b"];
data = _.sortBy(data, function (d) {
    var predicate = '';
    for (var i = 0; i < properties.length; i++)
    {
        predicate += (i == properties.length - 1 
                           ? 'd.' + properties[i]
                           : 'd.' + properties[i] + ' + ')
    }
    return eval(predicate)
});

It works and you can see it in Plunker

0

If the problem is an integer is converted to a string, add zeroes before the integer to make it have the same length as the longest in the collection:

var maxLength = _.reduce(data, function(result, item) {
    var bString = _.toString(item.sortData.b);
    return result > bString.length ? result : bString.length;            
}, 0);

_.sortBy(data, function(item) {
    var bString = _.toString(item.sortData.b);
    if(maxLength > bString.length) {
        bString = [new Array(maxLength - bString.length + 1).join('0'), bString].join('');
    }

    return [item.sortData.a, bString];
});
0

I've found a good way to sort array by multiple nested fields.

const array = [
{id: '1', name: 'test', properties: { prop1: 'prop', prop2: 'prop'}},
{id: '2', name: 'test2', properties: { prop1: 'prop second', prop2: 'prop second'}}
]

I suggest to use 'sorters' object which will describe a key and sort order. It's comfortable to use it with some data table.

const sorters = {
 'id': 'asc',
 'properties_prop1': 'desc',//I'm describing nested fields with '_' symbol
}

dataSorted = orderBy(array, Object.keys(sorters).map(sorter => {
    return (row) => {
        if (sorter.includes('_')) { //checking for nested field
            const value = row["properties"][sorter.split('_')[1]];
            return value || null; 
        };
        return row[sorter] || null;// checking for empty values
    };
}), Object.values(sorters));

This function will sort an array with multiple nested fields, for the first arguments it takes an array to modify, seconds one it's actually an array of functions, each function have argument that actually an object from 'array' and return a value or null for sorting. Last argument of this function is 'sorting orders', each 'order' links with functions array by index. How the function looks like simple example after mapping:

 orderBy(array, [(row)  =>  row[key] || null, (row)  =>  row[key] || null , (row)  =>  row[key] || null] , ['asc', 'desc', 'asc'])

P.S. This code can be improved, but I would like to keep it like this for better understanding.