0

I have an array of objects and I'm trying to sort them alphanumerically, take the following example:

var objs = {
    'obj1': {'name': 'Object21'},
    'obj2': {'name': 'Object140'},
    'obj3': {'name': 'Object28'},
    'obj4': {'name': 'Object251'}
};

When calling _.sortBy(objs, function(obj) { return obj.name; } the output is:

  1. Object140
  2. Object21
  3. Object251
  4. Object28

How can I order this alphanumerically using Underscore? I know I could create a separate array using just the names but is there a better way of doing this using Underscore without creating an additional variable?

Rewt0r
  • 73
  • 2
  • 10
  • 2
    That's not an array of objects. That's an object with nested objects. Since `for-in` doesn't guarantee an order, there's no way to sort the nested objects unless you do turn it into an array of objects. – cookie monster Aug 03 '14 at 02:23
  • @cookiemonster: He is turning them into an array; he wants a different comparator. – SLaks Aug 03 '14 at 02:25
  • ...however, if your only concern is the order you're getting, then you'll need to convert the text to actual numbers and use that instead. Try `return parseInt(obj.name.replace("Object", ""), 10);`. – cookie monster Aug 03 '14 at 02:25
  • Apologies, what if the Object is converted to an array of objects using `_.toArray(objs);` – Rewt0r Aug 03 '14 at 02:25
  • @cookiemonster that won't work as the object name is user input, I just need to know how it can be sorted alphanumerically. – Rewt0r Aug 03 '14 at 02:27
  • Maybe I'm not understanding. It seems that you want the sorting to be purely numeric, ignoring the alphabet characters. If so, there are other ways to extract the number if the text isn't known. If not, then I guess I just don't understand the issue. – cookie monster Aug 03 '14 at 02:30
  • No I want it to be alphanumeric as per @Marco Bonelli's answer. – Rewt0r Aug 03 '14 at 02:37
  • That's not alphanumeric. If you're getting rid of the letters, then it's just numeric. Unless you're saying you want the numbers to be evaluated as text, though that would mean that you'll get the same result as in your question. – cookie monster Aug 03 '14 at 02:48
  • Alphanumeric will order them as in that answer.. http://en.wikipedia.org/wiki/Alphanumeric – Rewt0r Aug 03 '14 at 02:50
  • Don't know why this is so hard. Do you want the text before the number to be included in the sort or not? Maybe you should give a better example in the question and show how it should look after it's sorted. – cookie monster Aug 03 '14 at 02:54
  • I've updated my question as I've found an answer via Google. Thanks for your time :-) – Rewt0r Aug 03 '14 at 03:07

3 Answers3

6

I've managed to find a solution to this myself using Google :-) here's what I used for those that need this in future and it's actually called "Natural Sorting"

use by calling _.sortByNat(objs, function(obj) { return obj.name; })

/*
* Backbone.js & Underscore.js Natural Sorting
*
* @author Kevin Jantzer <https://gist.github.com/kjantzer/7027717>
* @since 2013-10-17
* 
* NOTE: make sure to include the Natural Sort algorithm by Jim Palmer (https://github.com/overset/javascript-natural-sort)
*/

// add _.sortByNat() method
_.mixin({

    sortByNat: function(obj, value, context) {
        var iterator = _.isFunction(value) ? value : function(obj){ return obj[value]; };
        return _.pluck(_.map(obj, function(value, index, list) {
          return {
            value: value,
            index: index,
            criteria: iterator.call(context, value, index, list)
          };
        }).sort(function(left, right) {
          var a = left.criteria;
          var b = right.criteria;
          return naturalSort(a, b);
        }), 'value');
    }
});

/*
* Natural Sort algorithm for Javascript - Version 0.7 - Released under MIT license
* Author: Jim Palmer (based on chunking idea from Dave Koelle)
* https://github.com/overset/javascript-natural-sort
*/
function naturalSort (a, b) {
    var re = /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi,
        sre = /(^[ ]*|[ ]*$)/g,
        dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,
        hre = /^0x[0-9a-f]+$/i,
        ore = /^0/,
        i = function(s) { return naturalSort.insensitive && (''+s).toLowerCase() || ''+s },
        // convert all to strings strip whitespace
        x = i(a).replace(sre, '') || '',
        y = i(b).replace(sre, '') || '',
        // chunk/tokenize
        xN = x.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'),
        yN = y.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'),
        // numeric, hex or date detection
        xD = parseInt(x.match(hre)) || (xN.length != 1 && x.match(dre) && Date.parse(x)),
        yD = parseInt(y.match(hre)) || xD && y.match(dre) && Date.parse(y) || null,
        oFxNcL, oFyNcL;
    // first try and sort Hex codes or Dates
    if (yD)
        if ( xD < yD ) return -1;
        else if ( xD > yD ) return 1;
    // natural sorting through split numeric strings and default strings
    for(var cLoc=0, numS=Math.max(xN.length, yN.length); cLoc < numS; cLoc++) {
        // find floats not starting with '0', string or 0 if not defined (Clint Priest)
        oFxNcL = !(xN[cLoc] || '').match(ore) && parseFloat(xN[cLoc]) || xN[cLoc] || 0;
        oFyNcL = !(yN[cLoc] || '').match(ore) && parseFloat(yN[cLoc]) || yN[cLoc] || 0;
        // handle numeric vs string comparison - number < string - (Kyle Adams)
        if (isNaN(oFxNcL) !== isNaN(oFyNcL)) { return (isNaN(oFxNcL)) ? 1 : -1; }
        // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
        else if (typeof oFxNcL !== typeof oFyNcL) {
            oFxNcL += '';
            oFyNcL += '';
        }
        if (oFxNcL < oFyNcL) return -1;
        if (oFxNcL > oFyNcL) return 1;
    }
    return 0;
}

// extend Array to have a natural sort
Array.prototype.sortNat = function(){
    return Array.prototype.sort.call(this, naturalSort)
}
Rewt0r
  • 73
  • 2
  • 10
1

You will need to create your own iterator function and then use it, you can't actually do this with the iterator function, but you can get close to it:

var objs = {
    'obj1': {'name': 'Object21'},
    'obj2': {'name': 'Object140'},
    'obj3': {'name': 'Object28'},
    'obj4': {'name': 'AnObject251'}
};

_.sortBy(objs, function(obj) {
    var cc = [], s = obj.name;
    for(var i = 0, c; c = s.charAt(i); i++) 
        c == +c ? cc.push(+c) : cc.push(c.charCodeAt(0));
    return +cc.join('');
});

> Object21
  Object28
  Object140
  AnObject251

"AnObject251" goes on the last place because of its length.

Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128
  • Thanks for your suggestion, this won't work because I have no idea what text is going to be before the numeric characters as these are user input. So if a user has Obj123 Obj111 and Object123 Object111 this wouldn't work. – Rewt0r Aug 03 '14 at 02:35
  • Pretty impossible, answer edited, but I can't get this to work better than now. – Marco Bonelli Aug 03 '14 at 03:06
  • I'll try this out, I've just edited my question as I managed to find an alternate answer via Google just before you updated this. Thanks for you help :-) – Rewt0r Aug 03 '14 at 03:06
1

You need the Alphanum sorting algorithm and an elegant implementation in JavaScript:

function alphanum(a, b) {
  function chunkify(t) {
    var tz = [], 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) {
        tz[++y] = "";
        n = m;
      }
      tz[y] += j;
    }
    return tz;
  }

  var aa = chunkify(a);
  var bb = chunkify(b);

  for (x = 0; aa[x] && bb[x]; x++) {
    if (aa[x] !== bb[x]) {
      var c = Number(aa[x]), d = Number(bb[x]);
      if (c == aa[x] && d == bb[x]) {
        return c - d;
      } else return (aa[x] > bb[x]) ? 1 : -1;
    }
  }
  return aa.length - bb.length;
}
Belphegor
  • 4,456
  • 11
  • 34
  • 59