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


115

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

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”进行排序?

Answers:


250

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


12
有一篇博客文章对此进行了扩展,并提供了有关排序升序和降序属性的良好信息。
Alex C

4
这里可以找到链式排序的更简单解决方案。公平地说,看起来这些帖子是在给出这些答案之后写的,但是在尝试使用以上答案中的代码并失败之后,它帮助我弄清楚了这一点。
Mike Devenney '16

1
您确定患者[0] .name和患者[1] .roomNumber应该在那里有索引吗?病人不是一个数组...
StinkyCat '18

[0]分度器是必需的,因为在原来的实例中,patients是一个数组的数组。这也是为什么在另一条评论中提到的博客文章中的“简单解决方案”在这里不起作用的原因。
罗里·麦克劳德

1
@ac_fire这是一个现已失效的链接的存档:archive.is/tiatQ
lustig

52

在这些情况下,我有时会使用一个怪诞的技巧:以一种可组合结果的方式组合属性:

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

但是,就像我说的那样,这很不客气。要正确执行此操作,您可能需要实际使用核心JavaScript sort方法

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

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


非常感谢该解决方案最终使用了第二种解决方案,因为我的属性可以是字符串和数字。因此,似乎没有一种简单的本地方法可以对数组进行排序?
克里斯蒂安·R

3
为什么return [patient[0].roomNumber, patient[0].name];没有这些还不够join
Csaba Toth 2014年

1
通用解决方案的链接似乎已损坏(或者我无法通过我们的代理服务器访问它)。你能把它张贴在这里吗?
Zev Spitz

此外,如何做compare的句柄值不属于原始值- undefinednull或素色的物体?
Zev Spitz

仅供参考,仅当您确保数组中所有项目的每个值的str长度均相同时,此技巧才有效。
miex

32

我知道我参加晚会很晚,但是我想为那些已经提出了更清洁,更快解决方案的人添加此功能。您可以按从最不重要的属性到最重要的属性的顺序将sortBy调用链接起来。在下面的代码中,我创建了一个新的患者数组,该数组按名称RoomNumber中从原始数组“ 患者”中排序。

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

4
即使您迟到了,您仍然是正确的:)谢谢!
艾伦·吉卡穆

3
很好,很干净。
杰森·图兰

11

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

测试负载会很有趣...


认真地,这就是答案
RadekDuchoň19/

7

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

这里的一个解决方案,快速,高效,容易地允许反向排序,并且可以与被使用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 } ]

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



2

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

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/


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]
})

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


1

您可以在迭代器中串联要排序的属性:

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

或类似的东西。

注意:由于要将数字属性roomNumber转换为字符串,如果房间号> 10,则必须执行某些操作。否则,11将排在2之前。您可以用前导零填充以解决问题,即01代替1。


1

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

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

4
您确定orderBy在下划线吗?我在文档或.d.ts文件中看不到它。
Zachary Dow

1
下划线没有orderBy。
AfroMogli

1
_.orderBy可以,但是它是lodash库的一种方法,而不是下划线:lodash.com/docs/4.17.4#orderBy lodash通常是下划线的直接替代品,因此可能适合于OP。
Mike K

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.