17

Suppose I have an array

var arr = [1,5,"ahsldk",10,55,3,2,7,8,1,2,75,"abc","huds"];

and I try sorting it, I get something like ...

[1, 1, 10, 2, 2, 3, 5, 55, 7, 75, 8, "abc", "ahsldk", "huds"]

notice 10 is before 2, how can I have something more like

[1,1,2,2,3,5 ..., "abc", "ahs...",...]
Phrogz
  • 296,393
  • 112
  • 651
  • 745
Jiew Meng
  • 84,767
  • 185
  • 495
  • 805

10 Answers10

16

From http://snipplr.com/view/36012/javascript-natural-sort/ by mrhoo:

Array.prototype.naturalSort= function(){
    var a, b, a1, b1, rx=/(\d+)|(\D+)/g, rd=/\d+/;
    return this.sort(function(as, bs){
        a= String(as).toLowerCase().match(rx);
        b= String(bs).toLowerCase().match(rx);
        while(a.length && b.length){
            a1= a.shift();
            b1= b.shift();
            if(rd.test(a1) || rd.test(b1)){
                if(!rd.test(a1)) return 1;
                if(!rd.test(b1)) return -1;
                if(a1!= b1) return a1-b1;
            }
            else if(a1!= b1) return a1> b1? 1: -1;
        }
        return a.length- b.length;
    });
}

Or, from Alphanum: Javascript Natural Sorting Algorithm by Brian Huisman:

Array.prototype.alphanumSort = function(caseInsensitive) {
  for (var z = 0, t; t = this[z]; z++) {
    this[z] = [];
    var x = 0, y = -1, n = 0, i, j;

    while (i = (j = t.charAt(x++)).charCodeAt(0)) {
      var m = (i == 46 || (i >=48 && i <= 57));
      if (m !== n) {
        this[z][++y] = "";
        n = m;
      }
      this[z][y] += j;
    }
  }

  this.sort(function(a, b) {
    for (var x = 0, aa, bb; (aa = a[x]) && (bb = b[x]); x++) {
      if (caseInsensitive) {
        aa = aa.toLowerCase();
        bb = bb.toLowerCase();
      }
      if (aa !== bb) {
        var c = Number(aa), d = Number(bb);
        if (c == aa && d == bb) {
          return c - d;
        } else return (aa > bb) ? 1 : -1;
      }
    }
    return a.length - b.length;
  });

  for (var z = 0; z < this.length; z++)
    this[z] = this[z].join("");
}
Adriano
  • 19,463
  • 19
  • 103
  • 140
jball
  • 24,791
  • 9
  • 70
  • 92
  • Is there any difference between the 2? My 1st impression b4 testing them out were, maybe the 2nd (Opera) 1 will be more reliable, since its from Opera, however after testing, http://jsfiddle.net/sqcFD/, I found out that I got an error in that 1. Maybe the shorter 1 will work for me. – Jiew Meng Dec 07 '10 at 03:48
  • @jiewmeng I only tested the first one - I assumed (based on the depth of analysis and a quick glance over the code) that the second one was solid... Stick with the first one if it's working for you. – jball Dec 07 '10 at 05:02
  • Note `Array.prototype.alphanumSort` requires that the input array contain only strings. – gradbot Dec 07 '10 at 06:02
  • 1
    @JiewMeng "maybe the 2nd (Opera) 1 will be more reliable, since its from Opera" : it's not from Opera, it was only a blog by GreyWyvern (Brian Huisman) on the Opera blog hosting service. see http://web.archive.org/web/20130826203933/http://my.opera.com/GreyWyvern/blog/show.dml/1671288 – Adriano Sep 26 '14 at 11:45
  • 1
    Also note that Brian Huisman was inspired by Dave Koelle's Alphanum Algorithm on https://web.archive.org/web/20131005224909/http://www.davekoelle.com/alphanum.html – Adriano Sep 26 '14 at 11:48
15

Short and sweet, per the original question:

var arr = [1,5,"ahsldk",10,55,3,2,7,8,1,2,75,"abc","huds"];
arr.sort(function(a,b){
  var a1=typeof a, b1=typeof b;
  return a1<b1 ? -1 : a1>b1 ? 1 : a<b ? -1 : a>b ? 1 : 0;
});
// [1, 1, 2, 2, 3, 5, 7, 8, 10, 55, 75, "abc", "ahsldk", "huds"]

(Sort first by type, then by value.)


A more-full-featured natural sort:

var items = ['a1c', 'a01', 'a1', 'a13', 'a1a', 'a1b', 'a3b1', 'a1b0',
             'a1b3', 'a1b1', 'dogs', 'cats', 'hogs', 'a2', '2', '20',
             1, 13, 1.1, 1.13, '1.2', 'a'];
 
console.log(naturalSort(items))
 
function naturalSort(ary, fullNumbers) {
  var re = fullNumbers ? /[\d\.\-]+|\D+/g : /\d+|\D+/g;

  // Perform a Schwartzian transform, breaking each entry into pieces first
  for (var i=ary.length;i--;)
    ary[i] = [ary[i]].concat((ary[i]+"").match(re).map(function(s){
      return isNaN(s) ? [s,false,s] : [s*1,true,s];
    }));

  // Perform a cascading sort down the pieces
  ary.sort(function(a,b){
    var al = a.length, bl=b.length, e=al>bl?al:bl;
    for (var i=1;i<e;++i) {
      // Sort "a" before "a1"
      if (i>=al) return -1; else if (i>=bl) return 1;
      else if (a[i][0]!==b[i][0])
        return (a[i][1]&&b[i][1]) ?        // Are we comparing numbers?
               (a[i][0]-b[i][0]) :         // Then diff them.
               (a[i][2]<b[i][2]) ? -1 : 1; // Otherwise, lexicographic sort
    }
    return 0;
  });

  // Restore the original values into the array
  for (var i=ary.length;i--;) ary[i] = ary[i][0];
  return ary;
}

With naturalSort, pass true as the second parameter if you want "1.13" to sort before "1.2".

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • 1
    If I have a number in a string `"55"` it sorts wrongly, http://jsfiddle.net/8VjWL/, it generally works tho – Jiew Meng Dec 07 '10 at 04:07
  • 2
    @jiewmeng This was not part of your question. Further, if you have a number in a string...then you have a string and not a number, and you should be populating your array more precisely. :p (You could add `*1` or `parseFloat` as your first sort criteria if you really wanted, but I would encourage you to do this only if you really must accept arrays with numbers-as-strings.) – Phrogz Dec 07 '10 at 04:12
  • +1 definitely simpler code for the cleaner cases where no numbers are masquerading as strings. – jball Dec 07 '10 at 17:01
  • Although my answer is short, it is annoying to type all those fallback cases explicitly. I've written [Array.sortBy](http://phrogz.net/JS/Array.prototype.sortBy.js) as a convenience for this sort of thing. You would use it with this problem as: `arr.sortBy( function(o){ return [typeof o, o] } );` – Phrogz Dec 08 '10 at 18:10
  • 1
    short and sweet :) –  Dec 09 '16 at 08:52
  • This sort will fail for such array: `['a30', 3, 'a03', 13, 'a13', 'a1', 2, 'a2', 30, 1] – Jaro Mar 16 '17 at 22:44
  • @Jaro It will "fail" for lots of situations where you make up new requirements. :p (Things like `a03`, `a1`, `a13` are both not self-consistent and also were not part of the original question.) – Phrogz Mar 16 '17 at 22:47
  • Ok, that's fair. This solution is really good for given question and requirements. It's not true natural sort though. – Jaro Mar 17 '17 at 03:15
15

You could do this in one line using String.prototype.localCompare() and get the result you are looking for. Note that the numeric collation option is enabled.

var arr = [1,5,"ahsldk",10,55,3,2,7,8,1,2,75,"abc","huds"];

arr.sort((a,b) => ("" + a).localeCompare(b, undefined, {numeric: true}));

console.log(arr);
// [1, 1, 2, 2, 3, 5, 7, 8, 10, 55, 75, "abc", "ahsldk", "huds"]

Maybe add some logic to handle nulls.

Please be aware that this will only work with integer numbers. Floating point numbers will not be sorted the way you would hope.

vegemite4me
  • 6,621
  • 5
  • 53
  • 79
7

// Most natural sorts are for sorting strings, so file2 is sorted before file10.

If you are mixing in actual numbers you need to sort them to the front of the array, because negative numbers and digits separated by hyphens are a pain to interpret. Strings with leading zeroes need to be careful, so part002 will sort before part010.

var natSort=function(as, bs) {
    var a, b, a1, b1,
    rx=  /(\d+)|(\D+)/g, rd= /\d/, rz=/^0/;
    if(typeof as=='number' || typeof bs=='number'){
        if(isNaN(as))return 1;
        if(isNaN(bs))return -1;
        return as-bs;
    }
    a= String(as).toLowerCase();
    b= String(bs).toLowerCase();
    if(a=== b) return 0;
    if(!(rd.test(a) && rd.test(b))) return a> b? 1: -1;
    a= a.match(rx);
    b= b.match(rx);
    while(a.length && b.length){
        a1= a.shift();
        b1= b.shift();
        if(a1!== b1){
            if(rd.test(a1) && rd.test(b1)){
                return a1.replace(rz,'.0')- b1.replace(rz,'.0');
            }
            else return a1> b1? 1: -1;
        }
    }
    return a.length - b.length;
}

array.sort(natSort)
Igor F.
  • 2,649
  • 2
  • 31
  • 39
kennebec
  • 102,654
  • 32
  • 106
  • 127
6

This is a refined.

var arr = [1,5,"ahsldk",10,55,3,2,7,8,1,2,75,"56","abc","huds"];
    arr.sort(
                function (a,b){
                    if ( isNaN(a)&&isNaN(b)) return a<b?-1:a==b?0:1;//both are string
                    else if (isNaN(a)) return 1;//only a is a string
                    else if (isNaN(b)) return -1;//only b is a string
                    else return a-b;//both are num
                }
    );

result: 1|1|2|2|3|5|7|8|10|55|56|75|abc|ahsldk|huds|

jball
  • 24,791
  • 9
  • 70
  • 92
pinichi
  • 2,199
  • 15
  • 17
  • A quick profile session in chrome shows this answer as the fastest. It's marginally faster than Phrogz solution and a magnitude faster than either of jball's solutions. – gradbot Dec 07 '10 at 06:06
  • 1
    I measured a 10% speed increase by using temp vars `var as = isNaN(a), bs = isNaN(b); ` – gradbot Dec 07 '10 at 06:11
  • Easy see that regex swallows processing. Maybe the typeof either? – pinichi Dec 07 '10 at 07:22
  • Nice - hopefully this isn't being used in situations that are that performance critical, but a speedup from code that is as readable as this is good news all around. – jball Dec 07 '10 at 17:02
  • @pinichi - when testing it I noticed you used `a=b` in your string comparison function; I'm assuming that you meant `a==b` (which seems to work correctly for me) and are not using some arcane js assigment as comparison trick? – jball Dec 07 '10 at 17:13
2

If you have only alphabetical and integer items, you can stick with simple code:

var arr = [1,5,"ahsldk",10,55,3,2,7,8,1,2,75,"abc","huds"];
arr.sort(function(a, b)
{
    if (a == b)
        return 0;

    var n1 = parseInt(a, 10);
    var n2 = parseInt(b, 10);
    if (isNaN(n1) && isNaN(n2)) {
        //both alphabetical
        return (a > b) ? 1 : 0;
    }
    else if (!isNaN(n1) && !isNaN(n2)) {
        //both integers
        return (n1 > n2) ? 1 : 0;
    }
    else if (isNaN(n1) && !isNaN(n2)) {
        //a alphabetical and b is integer
        return 1;
    }

    //a integer and b is alphabetical
    return 0;
});

Working example: http://jsfiddle.net/25X2e/

Shadow The GPT Wizard
  • 66,030
  • 26
  • 140
  • 208
1

If you can always assume numbers and strings of unmixed alphas, I would just divide and conquer. slice out numbers into a new array using typeof. Sort both independently and then just join the two arrays.

Erik Reppen
  • 4,605
  • 1
  • 22
  • 26
1

I knew the following way also which might sort the array alphanumerically order.

const arr = [1, 5, "ahsldk", 10, 55, 3, 2, 7, 8, 1, 2, 75, "abc", "huds"];
arr.sort((a, b) => a - b || a.toString().localeCompare(b.toString()));
console.log(arr)
MH2K9
  • 11,951
  • 7
  • 32
  • 49
0
var sorted =  ['as', '21sasa0', 'bssll'].sort((a,b) => a.replace(/[0-9]/g,"").localeCompare(b.replace(/[0-9]/g,"")));
Adriaan
  • 17,741
  • 7
  • 42
  • 75
zafar
  • 1
  • 2
    Welcome to Stack Overflow! Please read [answer] and [edit] your question to contain an explanation as to why this code would actually solve the problem at hand. Always remember that you're not only solving the problem, but are also educating the OP and any future readers of this post. – Adriaan Jun 10 '22 at 08:06
0

An alternative to the other answers here, using a combination of localCompare with other things:

function naturalSortObjects(list, property) {
  function naturalCompare(a, b) {
    var ax = [], bx = [];

    a[property].replace(/(\d+)|(\D+)/g, function(_, $1, $2) { ax.push([$1 || Infinity, $2 || ""]) });
    b[property].replace(/(\d+)|(\D+)/g, function(_, $1, $2) { bx.push([$1 || Infinity, $2 || ""]) });

    while(ax.length && bx.length) {
      var an = ax.shift();
      var bn = bx.shift();
      var nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]);
      if(nn) return nn;
    }

    return ax.length - bx.length;
  }

  return list.sort(naturalCompare);
}