javascript - Natural sort, array of objects, multiple columns, reverse, etc - Stack Overflow

I desperately need to implement client side sorting that emulates sorting through our tastypie api, whi

I desperately need to implement client side sorting that emulates sorting through our tastypie api, which can take multiple fields and return sorted data. So if for example I have data like:

arr = [ 
  { name: 'Foo LLC',        budget: 3500,  number_of_reqs: 1040 }, 
  { name: '22nd Amendment', budget: 1500,  number_of_reqs: 2000 },
  { name: 'STS 10',         budget: 50000, number_of_reqs: 500  },
  ...
  etc.
]

and given columns to sort e.g.:['name', '-number_of_reqs'] it should sort by name (ascending) and number_of_reqs (descending). I can't get my head around this, first of all it has to be "natural sort", it supposed to be fairly easy to get if we're talking about sorting a single column, but I need to be able to sort in multiple.

Also I'm not sure why I'm getting different results (from the way how api does it) when using lodash's _.sortBy? Is _.sortBy not "natural" or it's our api broken?

Also I was looking for an elegant solution. Just recently started using Ramdajs, it's so freaking awesome. I bet it would be easier to build sorting I need using that? I've tried, still can't get it right. Little help?

upd:

I found this and using it with Ramda like this:

fn = Rpose(R.sort(naturalSort), R.pluck("name"))
fn(arr)

seems to work for flat array, yet I still need to find a way to apply it for multiple fields in my array

I desperately need to implement client side sorting that emulates sorting through our tastypie api, which can take multiple fields and return sorted data. So if for example I have data like:

arr = [ 
  { name: 'Foo LLC',        budget: 3500,  number_of_reqs: 1040 }, 
  { name: '22nd Amendment', budget: 1500,  number_of_reqs: 2000 },
  { name: 'STS 10',         budget: 50000, number_of_reqs: 500  },
  ...
  etc.
]

and given columns to sort e.g.:['name', '-number_of_reqs'] it should sort by name (ascending) and number_of_reqs (descending). I can't get my head around this, first of all it has to be "natural sort", it supposed to be fairly easy to get if we're talking about sorting a single column, but I need to be able to sort in multiple.

Also I'm not sure why I'm getting different results (from the way how api does it) when using lodash's _.sortBy? Is _.sortBy not "natural" or it's our api broken?

Also I was looking for an elegant solution. Just recently started using Ramdajs, it's so freaking awesome. I bet it would be easier to build sorting I need using that? I've tried, still can't get it right. Little help?

upd:

I found this and using it with Ramda like this:

fn = R.pose(R.sort(naturalSort), R.pluck("name"))
fn(arr)

seems to work for flat array, yet I still need to find a way to apply it for multiple fields in my array

Share edited Oct 13, 2014 at 0:19 iLemming asked Oct 12, 2014 at 8:37 iLemmingiLemming 36.3k61 gold badges198 silver badges316 bronze badges
Add a ment  | 

5 Answers 5

Reset to default 8
fn = R.pose(R.sort(naturalSort), R.pluck("name"))

seems to be working

Really? I would expect that to return a sorted array of names, not sort an array of objects by their name property.

Using sortBy unfortunately doesn't let us supply a custom parison function (required for natural sort), and bining multiple columns in a single value that pares consistently might be possible but is cumbersome.

I still don't know how to do it for multiple fields

Functional programming can do a lot here, unfortunately Ramda isn't really equipped with useful functions for parators (except R.parator). We need three additional helpers:

  • on (like the one from Haskell), which takes an a -> b transformation and a b -> b -> Number parator function to yield a parator on two as. We can create it with Ramda like this:

    var on = R.curry(function(map, cmp) {
        return R.useWith(cmp, map, map);
        return R.useWith(cmp, [map, map]); // since Ramda >0.18 
    });
    
  • or - just like ||, but on numbers not limited to booleans like R.or. This can be used to chain two parators together, with the second only being invoked if the first yields 0 (equality). Alternatively, a library like thenBy could be used for this. But let's define it ourselves:

    var or = R.curry(function(fst, snd, a, b) {
        return fst(a, b) || snd(a, b);
    });
    
  • negate - a function that inverses a parison:

    function negate(cmp) {
        return R.pose(R.multiply(-1), cmp);
    }
    

Now, equipped with these we only need our parison functions (that natural sort is an adapted version of the one you found, see also Sort Array Elements (string with numbers), natural sort for more):

var NUMBER_GROUPS = /(-?\d*\.?\d+)/g;
function naturalCompare(a, b) {
    var aa = String(a).split(NUMBER_GROUPS),
        bb = String(b).split(NUMBER_GROUPS),
        min = Math.min(aa.length, bb.length);

    for (var i = 0; i < min; i++) {
        var x = aa[i].toLowerCase(),
            y = bb[i].toLowerCase();
        if (x < y) return -1;
        if (x > y) return 1;
        i++;
        if (i >= min) break;
        var z = parseFloat(aa[i]) - parseFloat(bb[i]);
        if (z != 0) return z;
    }
    return aa.length - bb.length;
}
function stringCompare(a, b) {
    a = String(a); b = String(b);
    return +(a>b)||-(a<b);
}
function numberCompare(a, b) {
    return a-b;
}

And now we can pose exactly the parison on objects that you want:

fn = R.sort(or(on(R.prop("name"), naturalCompare),
               on(R.prop("number_of_reqs"), negate(numberCompare))));
fn(arr)

I think this works.

var arr = [
  { name: 'Foo LLC',        budget: 3500,  number_of_reqs: 1040 }, 
  { name: '22nd Amendment', budget: 1500,  number_of_reqs: 2000 },
  { name: 'STS 10',         budget: 50000, number_of_reqs: 5000 },
  { name: 'STS 10',         budget: 50000, number_of_reqs: 500  }
];

var columns = ['name', 'number_of_reqs'];

var NUMBER_GROUPS = /(-?\d*\.?\d+)/g;
var naturalSort = function (a, b, columnname) {
  var a_field1 = a[columnname],
      b_field1 = b[columnname],
      aa = String(a_field1).split(NUMBER_GROUPS),
      bb = String(b_field1).split(NUMBER_GROUPS),
      min = Math.min(aa.length, bb.length);

  for (var i = 0; i < min; i++) {
    var x = parseFloat(aa[i]) || aa[i].toLowerCase(),
        y = parseFloat(bb[i]) || bb[i].toLowerCase();
    if (x < y) return -1;
    else if (x > y) return 1;
  }

  return 0;
};

arr.sort(function(a, b) {
  var result;
  for (var i = 0; i < columns.length; i++) {
    result = naturalSort(a, b, columns[i]);
    if (result !== 0) return result; // or -result for decending
  }
  return 0; //If both are exactly the same
});

console.log(arr);

Bergi's answer is useful and quite interesting, but it changes the API you requested. Here's one that creates the API you were seeking:

var multisort = (function() {
    var propLt = R.curry(function(name, a, b) {
        return a[name] < b[name];
    });
    return function(keys, objs) {
        if (arguments.length === 0) {throw new TypeError('cannot sort on nothing');}
        var fns = R.map(function(key) {
            return key.charAt(0) === "-" ? 
                R.pipe(R.parator(propLt(R.substringFrom(1, key))), R.multiply(-1)) :
                R.parator(propLt(key));
        }, keys);
        var sorter = function(a, b) {
            return R.reduce(function(acc, fn) {return acc || fn(a, b);}, 0, fns);
        }
        return arguments.length === 1 ? R.sort(sorter) : R.sort(sorter, objs);
    };
}());

multisort(['name', '-number_of_reqs'], arr); //=> sorted clone

It's manually curried rather than calling R.curry because a fair bit of the work is involved in creating the separate sort functions, which could then be reused if you are sorting many lists with the same set of keys. If that's not a concern, this could be simplified a bit.

If you're willing to add another dependency to your project, @panosoft/ramda-utils es with a pareProps function that does exactly what the original question was asking for.

So, given your original example, to sort descending by budget and then by name, you could do something like this:

var props = ["-budget", "name"];
var parator = Ru.pareProps(props);
var sortedList = R.sort(parator, arr);

use the javascript native sort:

    

Array.prototype.multisort = function(columns) {
  var arr = this;
  arr.sort(function(a, b) {
    return pare(a, b, 0);
  });

  function pare(a, b, colindex) {
    if (colindex >= columns.length) return 0;

    var columnname = columns[colindex];
    var a_field1 = a[columnname];
    var b_field1 = b[columnname];
    var asc = (colindex % 2 === 0);

    if (a_field1 < b_field1) return asc ? -1 : 1;
    else if (a_field1 > b_field1) return asc ? 1 : -1;
    else return pare(a, b, colindex + 1);
  }
}



var arr = [{ name: 'Foo LLC',      budget: 3500,  number_of_reqs: 1040    }, 
           { name: '22nd Amendment',budget: 1500, number_of_reqs: 2000    }, 
           { name: 'STS 10',        budget: 50000,number_of_reqs: 5000    }, 
           { name: 'STS 10',        budget: 50000,number_of_reqs: 500    }];
arr.multisort(['name', 'number_of_reqs']);
if (window.console) window.console.log(arr);

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1741589345a4355223.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信