下划线:基于多个属性的sortBy()

时间:2013-05-07 19:13:14

标签: javascript sorting underscore.js

我正在尝试使用基于多个属性的对象对数组进行排序。即,如果两个对象之间的第一个属性相同,则应使用第二个属性来共同映射这两个对象。例如,请考虑以下数组:

var patients = [
             [{name: 'John', roomNumber: 1, bedNumber: 1}],
             [{name: 'Lisa', roomNumber: 1, bedNumber: 2}],
             [{name: 'Chris', roomNumber: 2, bedNumber: 1}],
             [{name: 'Omar', roomNumber: 3, bedNumber: 1}]
               ];

roomNumber属性对这些进行排序我将使用以下代码:

var sortedArray = _.sortBy(patients, function(patient) {
    return patient[0].roomNumber;
});

这样做很好,但我该如何处理以便'John'和'Lisa'能够正确分类?

12 个答案:

答案 0 :(得分:241)

sortBy表示它是一种稳定的排序算法,因此您应该能够先按第二个属性排序,然后再按第一个属性排序,如下所示:

var sortedArray = _(patients).chain().sortBy(function(patient) {
    return patient[0].name;
}).sortBy(function(patient) {
    return patient[0].roomNumber;
}).value();

当第二个sortBy发现John和Lisa具有相同的房间号时,它将按照它找到的顺序保留它们,第一个sortBy设置为“Lisa,John”。

答案 1 :(得分:49)

这是我在这些情况下有时使用的一个hacky技巧:以可以排序结果的方式组合属性:

var sortedArray = _.sortBy(patients, function(patient) {
  return [patient[0].roomNumber, patient[0].name].join("_");
});

但是,正如我所说的那样,这很糟糕。要正确执行此操作,您可能希望实际使用the core JavaScript sort method

patients.sort(function(x, y) {
  var roomX = x[0].roomNumber;
  var roomY = y[0].roomNumber;
  if (roomX !== roomY) {
    return compare(roomX, roomY);
  }
  return compare(x[0].name, y[0].name);
});

// General comparison function for convenience
function compare(x, y) {
  if (x === y) {
    return 0;
  }
  return x > y ? 1 : -1;
}

当然,这个会对您的数组进行排序。如果你想要一个排序的副本(比如_.sortBy会给你),首先克隆数组:

function sortOutOfPlace(sequence, sorter) {
  var copy = _.clone(sequence);
  copy.sort(sorter);
  return copy;
}

出于无聊,我刚刚写了一个通用解决方案(按任意数量的键排序):have a look

答案 2 :(得分:28)

我知道我迟到了,但我想为那些需要更清洁,更快速解决方案的人添加这个。您可以按最不重要的属性将sortBy调用链接到最重要的属性。在下面的代码中,我创建了一个新的患者数组,其中名称 RoomNumber 中从名为 patients 的原始数组中排序。

var sortedPatients = _.chain(patients)
  .sortBy('Name')
  .sortBy('RoomNumber')
  .value();

答案 3 :(得分:10)

btw你的患者初始化器有点奇怪,不是吗? 你为什么不初始化这个变量,因为这是一个真正的对象数组 - 你可以使用 _。flatten()来做,而不是作为一个数组的数组单个对象,也许是错字问题):

var patients = [
        {name: 'Omar', roomNumber: 3, bedNumber: 1},
        {name: 'John', roomNumber: 1, bedNumber: 1},
        {name: 'Chris', roomNumber: 2, bedNumber: 1},
        {name: 'Lisa', roomNumber: 1, bedNumber: 2},
        {name: 'Kiko', roomNumber: 1, bedNumber: 2}
        ];

我对名单进行了不同的排序,并将Kiko添加到Lisa的床上;只是为了好玩,看看会做些什么改变......

var sorted = _(patients).sortBy( 
                    function(patient){
                       return [patient.roomNumber, patient.bedNumber, patient.name];
                    });

检查已排序,你会看到这个

[
{bedNumber: 1, name: "John", roomNumber: 1}, 
{bedNumber: 2, name: "Kiko", roomNumber: 1}, 
{bedNumber: 2, name: "Lisa", roomNumber: 1}, 
{bedNumber: 1, name: "Chris", roomNumber: 2}, 
{bedNumber: 1, name: "Omar", roomNumber: 3}
]

所以我的答案是:在回调函数中使用数组 这与 Dan Tao 的答案非常相似,我只是忘记了连接(也许是因为我删除了独特项目的数组数组:))
使用您的数据结构,然后是:

var sorted = _(patients).chain()
                        .flatten()
                        .sortBy( function(patient){
                              return [patient.roomNumber, 
                                     patient.bedNumber, 
                                     patient.name];
                        })
                        .value();

并且测试负载会很有趣......

答案 4 :(得分:5)

这些答案都不是理想的,因为它是在排序中使用多个字段的通用方法。上面的所有方法效率都很低,因为它们要么需要多次对数组进行排序(在足够大的列表中可能会使速度降低很多),或者它们会生成大量的垃圾对象,这些垃圾对象需要清理(最终会减慢)该计划下来)。

这是一个快速,高效,易于反向排序的解决方案,可以与underscorelodash一起使用,也可以直接与Array.sort一起使用

最重要的部分是compositeComparator方法,它采用比较器函数数组并返回一个新的复合比较器函数。

/**
 * Chains a comparator function to another comparator
 * and returns the result of the first comparator, unless
 * the first comparator returns 0, in which case the
 * result of the second comparator is used.
 */
function makeChainedComparator(first, next) {
  return function(a, b) {
    var result = first(a, b);
    if (result !== 0) return result;
    return next(a, b);
  }
}

/**
 * Given an array of comparators, returns a new comparator with
 * descending priority such that
 * the next comparator will only be used if the precending on returned
 * 0 (ie, found the two objects to be equal)
 *
 * Allows multiple sorts to be used simply. For example,
 * sort by column a, then sort by column b, then sort by column c
 */
function compositeComparator(comparators) {
  return comparators.reduceRight(function(memo, comparator) {
    return makeChainedComparator(comparator, memo);
  });
}

您还需要一个比较器功能来比较您希望排序的字段。 naturalSort函数将在给定特定字段的情况下创建比较器。为反向排序编写比较器也很简单。

function naturalSort(field) {
  return function(a, b) {
    var c1 = a[field];
    var c2 = b[field];
    if (c1 > c2) return 1;
    if (c1 < c2) return -1;
    return 0;
  }
}

(到目前为止所有代码都是可重用的,例如可以保存在实用程序模块中)

接下来,您需要创建复合比较器。对于我们的示例,它看起来像这样:

var cmp = compositeComparator([naturalSort('roomNumber'), naturalSort('name')]);

这将按房间号排序,后跟名称。添加其他排序条件非常简单,不会影响排序的性能。

var patients = [
 {name: 'John', roomNumber: 3, bedNumber: 1},
 {name: 'Omar', roomNumber: 2, bedNumber: 1},
 {name: 'Lisa', roomNumber: 2, bedNumber: 2},
 {name: 'Chris', roomNumber: 1, bedNumber: 1},
];

// Sort using the composite
patients.sort(cmp);

console.log(patients);

返回以下内容

[ { name: 'Chris', roomNumber: 1, bedNumber: 1 },
  { name: 'Lisa', roomNumber: 2, bedNumber: 2 },
  { name: 'Omar', roomNumber: 2, bedNumber: 1 },
  { name: 'John', roomNumber: 3, bedNumber: 1 } ]

我更喜欢这种方法的原因是它允许在任意数量的字段上快速排序,不会产生大量垃圾或在排序中执行字符串连接,并且可以很容易地使用,以便在命令时对某些列进行反向排序列使用自然排序。

答案 5 :(得分:3)

来自http://janetriley.net/2014/12/sort-on-multiple-keys-with-underscores-sortby.html

简单示例(由@MikeDevenney提供)

<强>代码

var FullySortedArray = _.sortBy(( _.sortBy(array, 'second')), 'first');

使用您的数据

var FullySortedArray = _.sortBy(( _.sortBy(patients, 'roomNumber')), 'name');

答案 6 :(得分:2)

也许underscore.js或者只是Javascript引擎现在与编写这些答案时不同,但我能够通过返回排序键的数组来解决这个问题。

var input = [];

for (var i = 0; i < 20; ++i) {
  input.push({
    a: Math.round(100 * Math.random()),
    b: Math.round(3 * Math.random())
  })
}

var output = _.sortBy(input, function(o) {
  return [o.b, o.a];
});

// output is now sorted by b ascending, a ascending

在行动中,请看这个小提琴:https://jsfiddle.net/mikeular/xenu3u91/

答案 7 :(得分:2)

只需返回要排序的属性数组

ES6语法

var sortedArray = _.sortBy(patients, patient => [patient[0].name, patient[1].roomNumber])

ES5语法

var sortedArray = _.sortBy(patients, function(patient) { 
    return [patient[0].name, patient[1].roomNumber]
})

这没有将数字转换为字符串的任何副作用。

答案 8 :(得分:1)

您可以在迭代器中连接要排序的属性:

return [patient[0].roomNumber,patient[0].name].join('|');

或类似的东西。

注意:由于您要将数字属性roomNumber转换为字符串,因此如果您有房间号&gt;则必须执行某些操作。 10.否则11将在2之前到来。您可以使用前导零填充来解决问题,即01而不是1.

答案 9 :(得分:0)

我认为您最好使用_.orderBy代替sortBy

_.orderBy(patients, ['name', 'roomNumber'], ['asc', 'desc'])

答案 10 :(得分:0)

如果碰巧正在使用Angular,则可以在html文件中使用其数字过滤器,而不必添加任何JS或CSS处理程序。例如:

  No fractions: <span>{{val | number:0}}</span><br>

在该示例中,如果val = 1234567,它将显示为

  No fractions: 1,234,567

示例和进一步的指导,位于: https://docs.angularjs.org/api/ng/filter/number

答案 11 :(得分:0)

我不认为大多数答案真的有效,当然没有一个有效并且同时使用纯粹的下划线。

此答案提供了对多列的排序,并且能够在一个函数中反转其中一些列的排序顺序。

它还一步一步地构建在最终代码的基础上,因此您可能需要采用最后一个代码片段:


我已经将这个仅用于两列(首先按 a 排序,然后按 b 排序):

var array = [{a:1, b:1}, {a:1, b:0}, {a:2, b:2}, {a:1, b:3}];
_.chain(array)
 .groupBy(function(i){ return i.a;})
 .map(function(g){ return _.chain(g).sortBy(function(i){ return i.b;}).value(); })
 .sortBy(function(i){ return i[0].a;})
 .flatten()
 .value();

结果如下:

0: {a: 1, b: 0}
1: {a: 1, b: 1}
2: {a: 1, b: 3}
3: {a: 2, b: 2}

我相信这可以推广到两个以上......


另一个可能更快的版本:

var array = [{a:1, b:1}, {a:1, b:0}, {a:2, b:2}, {a:1, b:3}];
_.chain(array)
    .sortBy(function(i){ return i.a;})
    .reduce(function(prev, i){
        var ix = prev.length - 1;
        if(!prev[ix] || prev[ix][0].a !== i.a) {
         prev.push([]); ix++;
        }
        prev[ix].push(i);
        return prev;
    }, [])
    .map(function(i){ return _.chain(i).sortBy(function(j){ return j.b; }).value();})
    .flatten()
    .value();

以及它的参数化版本:

var array = [{a:1, b:1}, {a:1, b:0}, {a:2, b:2}, {a:1, b:3}];
function multiColumnSort(array, columnNames) {
    var col0 = columnNames[0],
        col1 = columnNames[1];
    return _.chain(array)
        .sortBy(function(i){ return i[col0];})
        .reduce(function(prev, i){
            var ix = prev.length - 1;
            if(!prev[ix] || prev[ix][0][col0] !== i[col0]) {
             prev.push([]); ix++;
            }
            prev[ix].push(i);
            return prev;
        }, [])
        .map(function(i){ return _.chain(i).sortBy(function(j){ return j[col1]; }).value();})
        .flatten()
        .value();
}
multiColumnSort(array, ['a', 'b']);

还有一个参数化版本适用于任意数量的列(似乎从第一次测试开始工作):

var array = [{a:1, b:1, c:9}, {a:1, b:1, c:3}, {a:2, b:2, c:10}, {a:1, b:3, c:0}];
function multiColumnSort(array, columnNames) {
    if(!columnNames || !columnNames.length || array.length === 1) return array;
    var col0 = columnNames[0];
    if(columnNames.length == 1) return _.chain(array).sortBy(function(i){ return i[col0]; }).value();
    
    return _.chain(array)
        .sortBy(function(i){ return i[col0];})
        .reduce(function(prev, i){
            var ix = prev.length - 1;
            if(!prev[ix] || prev[ix][0][col0] !== i[col0]) {
             prev.push([]); ix++;
            }
            prev[ix].push(i);
            return prev;
        }, [])
        .map(function(i){ return multiColumnSort(i, _.rest(columnNames, 1));})
        .flatten()
        .value();
}
multiColumnSort(array, ['a', 'b', 'c']);

如果您也希望能够反转列排序:

var array = [{a:1, b:1, c:9}, {a:1, b:1, c:3}, {a:2, b:2, c:10}, {a:1, b:3, c:0}];
function multiColumnSort(array, columnNames) {
    if(!columnNames || !columnNames.length || array.length === 1) return array;
    var col = columnNames[0],
        isString = !!col.toLocaleLowerCase,
        colName = isString ? col : col.name,
        reverse = isString ? false : col.reverse,
        multiplyWith = reverse ? -1 : +1;
    if(columnNames.length == 1) return _.chain(array).sortBy(function(i){ return multiplyWith * i[colName]; }).value();
    
    return _.chain(array)
        .sortBy(function(i){ return multiplyWith * i[colName];})
        .reduce(function(prev, i){
            var ix = prev.length - 1;
            if(!prev[ix] || prev[ix][0][colName] !== i[colName]) {
             prev.push([]); ix++;
            }
            prev[ix].push(i);
            return prev;
        }, [])
        .map(function(i){ return multiColumnSort(i, _.rest(columnNames, 1));})
        .flatten()
        .value();
}
multiColumnSort(array, ['a', {name:'b', reverse:true}, 'c']);

还支持函数:

var array = [{a:1, b:1, c:9}, {a:1, b:1, c:3}, {a:2, b:2, c:10}, {a:1, b:3, c:0}];
function multiColumnSort(array, columnNames) {
    if (!columnNames || !columnNames.length || array.length === 1) return array;
    var col = columnNames[0],
        isString = !!col.toLocaleLowerCase,
        isFun = typeof (col) === 'function',
        colName = isString ? col : col.name,
        reverse = isString || isFun ? false : col.reverse,
        multiplyWith = reverse ? -1 : +1,
        sortFunc = isFun ? col : function (i) { return multiplyWith * i[colName]; };

    if (columnNames.length == 1) return _.chain(array).sortBy(sortFunc).value();

    return _.chain(array)
        .sortBy(sortFunc)
        .reduce(function (prev, i) {
            var ix = prev.length - 1;
            if (!prev[ix] || (isFun ? sortFunc(prev[ix][0]) !== sortFunc(i) : prev[ix][0][colName] !== i[colName])) {
                prev.push([]); ix++;
            }
            prev[ix].push(i);
            return prev;
        }, [])
        .map(function (i) { return multiColumnSort(i, _.rest(columnNames, 1)); })
        .flatten()
        .value();
}
multiColumnSort(array, ['a', {name:'b', reverse:true}, function(i){ return -i.c; }]);