121

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

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”?

4

12 回答 12

259

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”。

于 2013-08-15T22:17:07.907 回答
52

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

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

但是,正如我所说,这很 hacky。要正确执行此操作,您可能需要实际使用核心 JavaScriptsort方法

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;
}

出于无聊,我也为此编写了一个通用解决方案(按任意数量的键排序):看看.

于 2013-05-07T19:22:11.437 回答
36

我知道我参加聚会迟到了,但我想为那些需要更清洁、更快速的解决方案的人添加这个,这些解决方案已经建议过。您可以按照最不重要的属性到最重要的属性的顺序链接 sortBy 调用。在下面的代码中,我创建了一个新的患者数组,该数组在RoomNumber中按Name排序,来自名为patients的原始数组。

var sortedPatients = _.chain(patients)
  .sortBy('Name')
  .sortBy('RoomNumber')
  .value();
于 2016-02-24T20:20:26.790 回答
12

顺便说一句,您的患者初始化程序有点奇怪,不是吗?你为什么不把这个变量初始化为这个 - 作为一个真正的对象数组- 你可以使用_.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();

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

于 2014-01-22T23:38:52.663 回答
7

这些答案都不适合作为在排序中使用多个字段的通用方法。上述所有方法都是低效的,因为它们要么需要对数组进行多次排序(在足够大的列表上可能会大大减慢速度),要么它们会生成大量的垃圾对象,VM 需要清理它们(并最终减慢程序下来)。

这是一个快速、高效、轻松允许反向排序的解决方案,并且可以与underscoreor一起使用lodash,或直接与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 } ]

我喜欢这种方法的原因是它允许对任意数量的字段进行快速排序,不会产生大量垃圾或在排序内执行字符串连接,并且可以轻松使用,以便某些列反向排序,而顺序列使用自然种类。

于 2016-09-15T13:25:24.403 回答
5

来自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');
于 2017-11-24T07:19:28.027 回答
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]
})

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

于 2018-02-14T01:53:00.730 回答
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/

于 2017-08-24T04:35:57.690 回答
1

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

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

或等效的东西。

注意:由于您将数字属性 roomNumber 转换为字符串,如果房间号 > 10,您将不得不做一些事情。否则 11 将在 2 之前。您可以用前导零填充来解决问题,即 01 而不是1.

于 2013-05-07T19:18:58.597 回答
1

我认为你最好使用_.orderBy而不是sortBy

_.orderBy(patients, ['name', 'roomNumber'], ['asc', 'desc'])
于 2016-09-19T06:01:31.853 回答
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

于 2019-01-31T04:58:18.437 回答
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; }]);
于 2021-02-16T18:27:47.550 回答