107

I have an array of objects to sort. Each object has two parameters: Strength and Name

objects = []
object[0] = {strength: 3, name: "Leo"}
object[1] = {strength: 3, name: "Mike"}

I want to sort first by Strength and then by name alphabetically. I am using the following code to sort by the first parameter. How do I sort then by the second?

function sortF(ob1,ob2) {
  if (ob1.strength > ob2.strength) {return 1}
  else if (ob1.strength < ob2.strength){return -1}
  return 0;
};

Thanks for your help.

(I am using Array.sort() with the aforementioned sortF as the sort comparison function passed into it.)

Magne
  • 16,401
  • 10
  • 68
  • 88
Leonardo Amigoni
  • 2,237
  • 5
  • 25
  • 35

10 Answers10

135

Expand your sort function to be like this;

function sortF(ob1,ob2) {
    if (ob1.strength > ob2.strength) {
        return 1;
    } else if (ob1.strength < ob2.strength) { 
        return -1;
    }

    // Else go to the 2nd item
    if (ob1.name < ob2.name) { 
        return -1;
    } else if (ob1.name > ob2.name) {
        return 1
    } else { // nothing to split them
        return 0;
    }
}

A < and > comparison on strings is an alphabetic comparison.

Matt
  • 74,352
  • 26
  • 153
  • 180
95

This little function is often handy when sorting by multiple keys:

cmp = function(a, b) {
    if (a > b) return +1;
    if (a < b) return -1;
    return 0;
}

or, more concisely,

cmp = (a, b) => (a > b) - (a < b)

Which works because in javascript:

true - true // gives 0
false - false // gives 0
true - false // gives 1
false - true // gives -1

Apply it like this:

array.sort(function(a, b) { 
    return cmp(a.strength,b.strength) || cmp(a.name,b.name)
})

Javascript is really missing Ruby's spaceship operator, which makes such comparisons extremely elegant.

Magne
  • 16,401
  • 10
  • 68
  • 88
georg
  • 211,518
  • 52
  • 313
  • 390
  • 25
    *cough* Perl's spaceship operator. – Richard Simões Mar 24 '14 at 20:32
  • 1
    Thank you. I've been search for this kernel of knowledge for a while now... I just didn't know how to word it. Especially the spaceship operator. – Brandon LeBlanc Mar 24 '17 at 20:33
  • Love this, implemented into an app. Would this work with 3 keys also? I'm not there yet, but might need a 3rd sorting. – Andy Smith Aug 14 '18 at 05:44
  • 2
    @AndySmith: sure, `cmp(a, b) || cmp(c, d) || cmp(e, f)` etc etc – georg Aug 14 '18 at 12:23
  • Love this, but your edit (the "more concise" version) doesn't quite seem to work for me. I think you're returning a boolean from that, where you actually need one of _three_ values (+1, -1, 0). @georg – Dave Salomon Apr 03 '19 at 05:27
  • @DaveSalomon, works for me but I'm no expert with this: `(30 > 40) - (30 < 40) -1 (30 > 30) - (30 < 30) 0 (40 > 30) - (40 < 30) 1` – Andy B Nov 19 '19 at 22:00
  • 2
    @AndyB: it has been fixed since then ;) – georg Nov 19 '19 at 22:02
  • When not comparing strings, one could even more tersely do `array.sort((a, b) => { return a.x - b.x || a.y - b.y })`, and even add `|| a.z - b.z` and so on for sorting by a 3rd criteria, etc. for sorting by more criterias. Note that with strings, you cannot compare by using `-` sign but have to compare by using `<` or `>` or `a.localeCompare(b)`. – Magne May 07 '20 at 18:15
49

You could chain the sort order with logical OR.

objects.sort(function (a, b) {
    return a.strength - b.strength || a.name.localeCompare(b.name);
});
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • That's brilliant! But what about the case where strength can be undefined, and we want all objects with defined strength to go first. Is there any solution that's as elegant as this? – Nan Li Oct 06 '16 at 23:59
  • maybe something like this: return ((a.strength || Number.MAX_VALUE) - (b.strength || Number.MAX_VALUE)) || a.name.localeCompare(b.name); – Nan Li Oct 07 '16 at 00:36
  • 1
    yes, you could add a default value for nonexistent parts like `return ((a.strength || Infinity) - (b.strength || Infinity)) || a.name.localeCompare(b.name);`. but that relies on the possible value of `strength`. – Nina Scholz Oct 07 '16 at 06:01
  • 1
    works like a charm `return a.z - b.z || a.x - b.x || a.y - b.y` – bresleveloper Nov 01 '17 at 18:29
15

When I was looking for an answer to this very question, the answers I found on StackOverflow weren't really what I hoped for. So I created a simple, reusable function that does exactly this. It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style. https://github.com/Teun/thenBy.js

PS. This is the second time I post this. The first time was removed by a moderator saying "Please don't make promotional posts for your own work". I'm not sure what the rules are here, but I was trying to answer this question. I'm very sorry that it is my own work. Feel free to remove again, but please point me to the rule involved then.

zhulien
  • 5,145
  • 3
  • 22
  • 36
Teun D
  • 5,045
  • 1
  • 34
  • 44
  • You should put a firstname, lastname example in the Readme.md as everyone is after that ^^, I will give this lib a try, looks good. – Christophe Roussy Jul 25 '17 at 11:50
  • 1
    Yeah, good point, I guess. It is tempting to show really arbitrary combinations of fields, but the readme should indeed start with the most common looked after scenario. Thanks for the tip. – Teun D Aug 28 '17 at 08:27
  • 3
    This is great. I cannot understand why the moderators would remove something so useful. If, it required purchasing, then that might be different, although, I guess it’s up to us to decide. We are all grown ups here and can make intelligent decisions. – Charles Robertson Dec 21 '18 at 22:38
  • I can't speak for the mods, but in addition to self-promotion my guess why they removed it: [link-only answers don't contain an answer](https://meta.stackexchange.com/a/8259/313196). Copying the 40 lines of library source code into your answer probably isn't ideal either, but copying a few lines of code or describing the algorithm implemented in the library would make the contents of your answer stand by itself. – Carl Walsh Jan 14 '20 at 06:40
5

steve's answer, but prettier.

objects.sort(function(a,b)
{
  if(a.strength > b.strength) {return  1;}
  if(a.strength < b.strength) {return -1;}
  if(a.name     > b.name    ) {return  1;}
  if(a.name     < b.name    ) {return -1;}
  return 0;
}
rocketsarefast
  • 4,072
  • 1
  • 24
  • 18
1

Find 'sortFn' function below. This function sorts by unlimited number of parameters(such as in c#: SortBy(...).ThenBy(...).ThenByDesc(...)).

function sortFn() {
    var sortByProps = Array.prototype.slice.call(arguments),
        cmpFn = function(left, right, sortOrder) {
            var sortMultiplier = sortOrder === "asc" ? 1 : -1;

            if (left > right) {
                return +1 * sortMultiplier;
            }
            if (left < right) {
                return -1 * sortMultiplier;
            }
            return 0;
        };


    return function(sortLeft, sortRight) {
        // get value from object by complex key
        var getValueByStr = function(obj, path) {
            var i, len;

            //prepare keys
            path = path.replace('[', '.');
            path = path.replace(']', '');
            path = path.split('.');

            len = path.length;

            for (i = 0; i < len; i++) {
                if (!obj || typeof obj !== 'object') {
                    return obj;
                }
                obj = obj[path[i]];
            }

            return obj;
        };

        return sortByProps.map(function(property) {
            return cmpFn(getValueByStr(sortLeft, property.prop), getValueByStr(sortRight, property.prop), property.sortOrder);
        }).reduceRight(function(left, right) {
            return right || left;
        });
    };
}

var arr = [{
    name: 'marry',
    LocalizedData: {
        'en-US': {
            Value: 10000
        }
    }
}, {
    name: 'larry',
    LocalizedData: {
        'en-US': {
            Value: 2
        }
    }
}, {
    name: 'marry',
    LocalizedData: {
        'en-US': {
            Value: 100
        }
    }
}, {
    name: 'larry',
    LocalizedData: {
        'en-US': {
            Value: 1
        }
    }
}];
document.getElementsByTagName('pre')[0].innerText = JSON.stringify(arr)

arr.sort(sortFn({
    prop: "name",
    sortOrder: "asc"
}, {
    prop: "LocalizedData[en-US].Value",
    sortOrder: "desc"
}));

document.getElementsByTagName('pre')[1].innerText = JSON.stringify(arr)
pre {
    font-family: "Courier New" Courier monospace;
    white-space: pre-wrap;
}
Before:
<pre></pre>
Result:
<pre></pre>
Ihor Shubin
  • 10,628
  • 3
  • 25
  • 33
1
function sortF(ob1,ob2) {
  if (ob1.strength > ob2.strength) {return 1}
  else if (ob1.strength < ob2.strength) {return -1}
  else if (ob1.name > ob2.name) {return 1}
  return -1;
};

EDIT: Sort by strength, then if strength is equal, sort by name. The case where strength and name are equal in both objects doesn't need to be accounted for seperately, since the final return of -1 indicates a less-than-or-equal-to relationship. The outcome of the sort will be correct. It might make it run faster or slower, I don't know. If you want to be explicit, just replace

return -1;

with

else if (ob1.name < ob2.name) {return -1}
return 0;
steve
  • 11
  • 2
0

With ES6 you can do

array.sort(function(a, b) { 
 return SortFn(a.strength,b.strength) || SortFn(a.name,b.name)
})

 private sortFn(a, b): number {
    return a === b ? 0 : a < b ? -1 : 1;
}
Josf
  • 776
  • 1
  • 8
  • 21
0

Here is the function I use. It will do an arbitrary number.

function Sorter(){
  
  var self = this;
  this.sortDefs = [];
  
  for (let i = 0; i < arguments.length; i++) {
  // Runs 5 times, with values of step 0 through 4.
    this.sortDefs.push(arguments[i]);
  }
  
  this.sort = function(a, b){
  
    for (let i = 0; i < self.sortDefs.length; i++) {

        if (a[self.sortDefs[i]] < b[self.sortDefs[i]]) { 
          return -1;
        } else if (a[self.sortDefs[i]] > b[self.sortDefs[i]]) {
          return 1
        }
    }
    
    return 0;
  }
}

data.sort(new Sorter('category','name').sort);
Leslie Marshall
  • 101
  • 1
  • 3
-1

In 2018 you can use just sort() ES6 function, that do exactly, what you want. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

Kamil Naja
  • 6,267
  • 6
  • 33
  • 47