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?

11
  • 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. Commented Aug 3, 2014 at 2:23
  • @cookiemonster: He is turning them into an array; he wants a different comparator. Commented Aug 3, 2014 at 2: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);. Commented Aug 3, 2014 at 2:25
  • Apologies, what if the Object is converted to an array of objects using _.toArray(objs); Commented Aug 3, 2014 at 2:25
  • @cookiemonster that won't work as the object name is user input, I just need to know how it can be sorted alphanumerically. Commented Aug 3, 2014 at 2:27

3 Answers 3

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)
}
Sign up to request clarification or add additional context in comments.

2 Comments

this does NOT work. "9B" will be sorted BEFORE "9A".
@Kinesias I have tested this and it works, 9A comes before 9B
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.

3 Comments

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.
Pretty impossible, answer edited, but I can't get this to work better than now.
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 :-)
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;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.