views:

48

answers:

2

I've got an array of Javascript objects that I'd like to cross-compatibly sort by a property that is always a positive integer with an optional single letter at the end. I'm looking for a solution that works in at least Firefox 3 and Internet Explorer 8. The closest I've come to such a sort function is the following:

var arrayOfObjects = [{id: '1A', name: 'bar', size: 'big'}, {id: '1C', name: 'bar', size: 'small'}, {id: '1', name: 'foo', size: 'big'}, {id: '1F', name: 'bar', size: 'big'}, {id: '1E', name: 'bar', size: 'big'}, {id: '1B', name: 'bar', size: 'small'}, {id: '1D', name: 'bar', size: 'big'}, {id: '1G', name: 'foo', size: 'small'},  {id: '3', name: 'foo', size: 'small'}, {id: '23', name: 'foo', size: 'small'}, {id: '2', name: 'foo', size: 'small'}, {id: '1010', name: 'foo', size: 'small'}, {id: '23C', name: 'foo', size: 'small'}, {id: '15', name: 'foo', size: 'small'}]

arrayOfObjects.sort(function(a, b){
    return (a.id < b.id ? -1 : a.id == b.id ? 0 : 1);
});

After being so sorted, printing out arrayOfObjects gives:

1, foo, big
1010, foo, small
15, foo, small
1A, bar, big
1B, bar, small
1C, bar, small
1D, bar, big
1E, bar, big
1F, bar, big
1G, foo, small
2, foo, small
23, foo, small
23C, foo, small
3, foo, small

However, I would like arrayOfObjects to print out in the order below:

1, foo, big
1A, bar, big
1B, bar, small
1C, bar, small
1D, bar, big
1E, bar, big
1F, bar, big
1G, foo, small
2, foo, small
3, foo, small
15, foo, small
23, foo, small
23C, foo, small
1010, foo, small

Given that, how could I fix the above function so that the objects sort by number as primary key and letter as secondary key? Thanks in advance for any help.

+3  A: 
arrayOfObjects.sort((function() {
  var splitter = /^(\d+)([A-Z]*)/;
  return function(a, b) {
    a = a.id.match(splitter); b = b.id.match(splitter);
    var anum = parseInt(a[1], 10), bnum = parseInt(b[1], 10);
    if (anum === bnum)
      return a[2] < b[2] ? -1 : a[2] > b[2] ? 1 : 0;
    return anum - bnum;
  }
})());

the idea is to split the keys into the numeric and string parts.

edit (oops got the "match" call backwards)

edit again @Ryan Tenney wisely suggests that the anonymous outer function isn't really necessary:

arrayOfObjects.sort(function(a, b) {
  var splitter = /^(\d+)([A-Z]*)/;
  a = a.id.match(splitter); b = b.id.match(splitter);
  var anum = parseInt(a[1], 10), bnum = parseInt(b[1], 10);
  if (anum === bnum)
    return a[2] < b[2] ? -1 : a[2] > b[2] ? 1 : 0;
  return anum - bnum;     
});

a little simpler.

Pointy
The regex literal doesn't add any cost to each iteration. You'd be better off getting rid of the outer self executing function and declaring splitter inside the inner function.
Ryan Tenney
Besides that, great answer. It is significantly more concise than the answer I was preparing to offer :)
Ryan Tenney
I have a thing about repeating regexes - it's not so much for performance as it is about maintenance. Maybe once in a hundred tries I'll get a regex right on the first go, so I want to minimize the number of times I repeat it. Of course I could have just typed it in as a `var` in the function body; I wasn't thinking much about that.
Pointy
Excellent, this works. Thank you.
I think you can change the first return to `return a[2] < b[2] ? -1 : 1;` and have it still sort fine.
CD Sanchez
@Daniel well that might make the sort *unstable* - a "stable" sort is one that leaves elements that were already in order (due to key equality) in the same order, while an "unstable" sort can permute those. I don't know for sure that that's true, but I always explicitly check for key equality so that a stable sort can really remain stable.
Pointy
@Pointy: IIRC I think Chrome's `sort` implementation is still unstable, so even if the two elements are equal their positions may still be swapped in the final state. The other browsers' sort implementations are stable though. Better safe than sorry I suppose.
CD Sanchez
A: 

You don't need to parse the integer out of a string of digits-

If the two strings of digits match, the value doesn't matter, you look at a possible letter.

If the digits don't match, subtracting one from the other coerces the numbers.

var rx=/^(\d+)(\D?)$/;

    arrayOfObjects.sort(function(a, b){ 
        var id_a= a.id.match(rx), id_b= b.id.match(rx);
        if(id_a[1]== id_b[1]){
            if(id_a[2]=== id_b[2]) return 0;
            else{
                if(!id_a[2]) return -1;
                if(!id_b[2]) return 1;
                return id_a[2]> id_b[2]? 1: -1;
            }
        }
        return id_a[1]-id_b[1];
    });
kennebec