81

I'm trying to sort an array of objects. I'd prefer not to write a custom sort method for each attribute.

Is there anyway I could extend the built-in array.sort() method to accept an extra parameter, describing the attribute to sort on? E.g.,

array.sort(function(a, b, attr) { return a.attr - b.attr; }, 'name');
kewlashu
  • 1,099
  • 10
  • 18
danwoods
  • 4,889
  • 11
  • 62
  • 90
  • Your example makes no sense, because you aren't actually using the `attr` parameter, you are simply getting the `attr` property of the objects. You probably want to change it to: `array.sort(function(a, b, attr) { return a[attr] - b[attr]; }, 'name');` – Jules Colle Dec 03 '20 at 08:15

7 Answers7

161

Write a function generator that accepts a property name:

function propComparator(prop) {
    return function(a, b) {
        return a[prop] - b[prop];
    }
}

arr.sort(propComparator('name'));

You can also save the sorters for later use, directly, or as parameters:

var compareNames = propComparator('name');
var compareFoos = propComparator('foo');
...
arr.sort(compareNames);
takesComparator(compareFoos);

sort sorts in-place, which may or may not be desirable.


(Updated for ES6, and so it actually works with different types.)

const arr = [
  { name: 'John', age: 92 },
  { name: 'Dave', age: 42 },
  { name: 'Justin', age: 3 }
]

const propComparator = (propName) =>
  (a, b) => a[propName] == b[propName] ? 0 : a[propName] < b[propName] ? -1 : 1

arr.sort(propComparator('name'))
console.log("By name", arr)

arr.sort(propComparator('age'))
console.log("By age", arr)
Dave Newton
  • 158,873
  • 26
  • 254
  • 302
  • Thanks Dave I was having trouble passing a variable to sort an array of objects and this worked perfectly! – Throttlehead Apr 16 '13 at 17:02
  • 3
    See it here live. http://jsfiddle.net/mivaas19/AMukX/ . Great answer; but Iam still wondering how it works. – agaase May 03 '13 at 11:54
  • 2
    @mivaas19 It works because you can pass function references around, whether immediate functions, like you see with jQuery event handlers a lot of the time, or named functions, where you just leave off the parentheses. It's a very powerful tool. – Dave Newton May 03 '13 at 12:44
10

Is this what you're looking for?

function sortByProperty(array, propertyName) {
    return array.sort(function (a, b) {
        return a[propertyName] - b[propertyName];
    });
}

var sortedByName = sortByProperty(myArray, "name");
Domenic
  • 110,262
  • 41
  • 219
  • 271
  • I'm not seeing a huge difference between the two versions; @DaveNewton, can you explain your comment? Also, thanks to both of you, both answers look like they'll work... – danwoods Dec 16 '11 at 17:17
  • @danwoods The difference is one of communication: I prefer the more functional-/OOP-looking approach that keeps the array as the primary actor in the statement: `arr.sort(how)` to me is more communicative than `how(arr, moreHow)`. Also, the generated functions act on anything that has properties with the name, instead of wrapping up an array-specific, or `sort`-function-specific, entity. – Dave Newton Dec 16 '11 at 17:21
  • One would give you a compare function (Daves), and the other uses an anonymous function. So if you want the compare function, use the one that gives it. – aepheus Dec 16 '11 at 17:22
6

Use prototypes to compare strings and numbers correctly

Array.prototype.sortAttr = function(attr,reverse) {
  var sorter = function(a,b) {
    var aa = a[attr];
    var bb = b[attr];
    if(aa+0==aa && bb+0==bb) return aa-bb; // numbers
    else return aa.localeCompare(bb); // strings
  }
  this.sort(function(a,b) {
    var result = sorter(a,b);
    if(reverse) result*= -1;
    return result;
  });
};

Example

var data = [
  {name: "Josh", age: 18},
  {name: "John", age: 17},
  {name: "Bob", age: 20},
  {name: 0, age: "error"}
];

data.sortAttr("name");
// data is now sorted by name
Jan Turoň
  • 31,451
  • 23
  • 125
  • 169
  • Prototypes or not isn't the issue with strings v. numbers, it's an issue of the sort function being provided. – Dave Newton Dec 16 '11 at 19:32
  • 1
    The code sometimes throws "too much recursion" exception. Modifying the sorter to use localeCompare() instead of the custom defined compare is advised. – d.popov Nov 08 '17 at 16:51
  • 1
    @d.popov What an archaeologist you are! 6 years ago localeCompare was buggy, perharps it is time to update now. Thanks for good catch, the recursion was horrible, I was so naive back then... – Jan Turoň Nov 08 '17 at 22:05
3

Is there anyway I could extend the built-in array.sort() method to accept an extra parameter

all above answers are good . but i thought of adding some info about partial functions

for more info see bind in MDN and partial Function or John Resig - partial function

Example from MDN :

function list() {
  return Array.prototype.slice.call(arguments);
}

var list1 = list(1, 2, 3); // [1, 2, 3]

//  Create a function with a preset leading argument
var leadingThirtysevenList = list.bind(undefined, 37);

var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]

here is an example from Google Closure

goog.partial = function(fn, var_args) {
  var args = Array.prototype.slice.call(arguments, 1);
  return function() {
    // Prepend the bound arguments to the current arguments.
    var newArgs = Array.prototype.slice.call(arguments);
    newArgs.unshift.apply(newArgs, args);
    return fn.apply(this, newArgs);
  };
};

to Use this function

    var fn=goog.partial(numberCompare,sortField,sortDirection);
    myarray.sort (fn);


    var numberCompare = function (sortField,sortDirection,value1,value2){
      // sort code goes here
    }
aked
  • 5,625
  • 2
  • 28
  • 33
2

In case someone needs ascending order, here is DaveNewton's solution with a reverse option

const sorton = (prop, asc=0) => {
    if(!asc) return (a, b) => a[prop] == b[prop] ? 0 : a[prop] < b[prop] ? -1 : 1
    else return (b, a) => a[prop] == b[prop] ? 0 : a[prop] < b[prop] ? -1 : 1
}

arr.sort(propComparator('age', 1))

stallingOne
  • 3,633
  • 3
  • 41
  • 63
0

In Typescript it is simple. I had created a little different sorting function for string sorting. Since Array.sort takes a function, you can also pass closure. So my sorting function is:

sortCaseInsensitive(a,b,column:string) {
  return (a[column]? a[column] : '').toLowerCase().
  localeCompare((b[column]? b[column] : '').toLowerCase());
}

And if my data is like

    [ 
{ value:'a', label:'a1'}
    { value:'b', label:'b1'} 
]

Then, you can call:

arrayList123.sort((a,b)=>myself.sortCaseInsensitive(a,b,'value'));

There you can pass any arguments. Sweet and simple.

-1

Actually Extending It

In order to actually extend Array.prototype.sort, we have a couple options:

  • Mutate its signature
  • Sort in multiplicity using Decorators | Adapters ( Parent Pattern: Wrapper )

I was in your same boat and decided to utilize the 2nd approach:

private sortAddresses = (a, b) => {
    let iPrimeFlag = this.sortAddressesByPrimaryFlag(a, b);
    let iAlphaNum = this.sortAddressesByAlphaNum(a, b);

    if (iPrimeFlag === 1) return 1;
    else return iAlphaNum;
};

private sortAddressesByPrimaryFlag(a, b) {
    if (b.primaryFlag > a.primaryFlag) return 1;
    if (b.primaryFlag < a.primaryFlag) return -1;
    return 0;
}

private sortAddressesByAlphaNum(a, b) {
    let aAddress = this.$.formatAddress(a);
    let bAddress = this.$.formatAddress(b);

    if (aAddress > bAddress) return 1;
    if (aAddress < bAddress) return -1;

    return 0;
}

Intent

I'm already calling this.addresses.sort(this.sortAddresses) in several places and I'd like to keep my ChangeCost low -- especially, knowing that we may get requirements to sort on even more heuristics.

So, in order to follow The Gang of Four's Two 'Rules of Thumb' --

Program to an interface, not an implementation.

and

Encapsulate what varies.

-- I decided to keep my signature the same and wrap my original method.

It would be useful if we didn't have to go through and change each line where we invoke this.addresses.sort. Instead, we'd like to be able to add an indefinite number of sorting "heuristics" to the action of sorting.

The goal is to prioritize address objects whose primaryFlag is 'Y' and then take the address string -- '0000 Some St, #0000, City, ST 00000' -- and sort these alphanumerically. Since 'Y' is > 'N', we'd like to move it up in the list, visually, by lowering its index. Sorting the address string alphanumerically says that if 'Colorado' is > 'Alabama', then we should bump 'Colorado' down in the list visually by increasing its index.

Usage

This is used for sorting Addresses by different values. One value, primaryFlag, is to denote if its the [ only ] default address; in my case, primaryFlag is a sting of 'Y' or 'N', not a boolean (ask my Back-End teammates why in the world?). The other value, this.$.formatAddress(a|b), takes these address [object Object]'s -- a and b -- and invokes formatAddress off of my Sandbox this.$.

The line if (iPrimeFlag === 1) return 1; is saying, "anytime primaryFlag is 1, just bump that toward the head (beginning) of the array, otherwise, do whatever the alphanumeric heuristic decides", which allows us to prioritize by one heuristic while falling back on another.

Also note, .bind(undefined, 'prop') is not being used in my actual code as I don't need it; this is just here for demonstrative purposes.

Now, I know I'm that person who provided some TypeScript -- lemme know if you don't understand what's happening in this code :)

Cheers!

Cody
  • 9,785
  • 4
  • 61
  • 46
  • Isn’t this just a completely normal comparison function that isn’t generic as the question asked? – Ry- Jan 29 '17 at 00:38
  • @Ryan, no isn't. I'm solving the Questioner's problem by *wrapping* methods instead of *binding* parameters. No thanks for down voting my answer just because you're hurt that you haven't solved your sorting issue. – Cody Jan 29 '17 at 00:52
  • No I’m pretty sure this is just a completely normal comparison function – Ry- Jan 29 '17 at 00:57
  • Suit yourself. But I think you could be missing something important here. – Cody Jan 30 '17 at 00:39
  • It can't *not* be a normal sorting method--all "sort" can use is a normal sorting method; that it's choosing between the results of two other normal sorting methods doesn't change that. It also doesn't appear to address the original question unless your suggestion is to write a sort function for every possible property name? – Dave Newton Mar 17 '22 at 13:21