Подчеркивание: 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;
});

Это нормально работает, но как мне поступить, чтобы «Джон» и «Лиза» были отсортированы правильно?

Ответы:


250

sortBy говорит, что это стабильный алгоритм сортировки, поэтому вы должны сначала сортировать по второму свойству, а затем снова сортировать по первому свойству, например:

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

Когда второй sortByобнаруживает, что у Джона и Лизы одинаковый номер комнаты, он сохранит их в том порядке, в котором они были найдены, который первый sortByустановил на «Лиза, Джон».


12
Есть сообщение в блоге, которое расширяет это и включает полезную информацию о сортировке свойств по возрастанию и убыванию.
Alex C

4
Здесь можно найти более простое решение для сортировки по цепочке . Честно говоря, похоже, что сообщение в блоге было написано после того, как были даны эти ответы, но оно помогло мне понять это после попытки использовать код из приведенного выше ответа и неудачи.
Майк Девенни,

1
Вы уверены, что у пациента [0] .name и пациента [1] .roomNumber должен быть индекс? пациент не массив ...
StinkyCat

[0]Индексатор требуется потому , что в исходном примере, patientsпредставляет собой массив из массивов. Вот почему «более простое решение» в сообщении блога, упомянутое в другом комментарии, здесь не сработает.
Рори МакЛауд

1
@ac_fire Вот архив этой теперь мертвой ссылки: archive.is/tiatQ
lustig

52

Вот хитрый прием, который я иногда использую в таких случаях: объедините свойства таким образом, чтобы результат можно было сортировать:

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

Однако, как я уже сказал, это довольно взломано. Чтобы сделать это правильно, вы, вероятно, захотите использовать основной sortметод JavaScript :

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

От скуки я просто написал общее решение (для сортировки по произвольному количеству клавиш) и для этого: посмотрите .


Большое спасибо за это решение, в конечном итоге я использовал второй, так как мои атрибуты могли быть как строками, так и числами. Кажется, нет простого собственного способа сортировки массивов?
Christian R

3
Почему return [patient[0].roomNumber, patient[0].name];без join?
Csaba Toth

1
Ссылка на ваше общее решение не работает (или, возможно, я не могу получить к нему доступ через наш прокси-сервер). Не могли бы вы разместить это здесь?
Зев Шпиц

Кроме того , как делает compareзначение ручки , которые не являются примитивными значениями - undefined, nullили простых объекты?
Зев Шпиц

К вашему сведению, этот хакер работает только в том случае, если вы убедитесь, что длина str каждого значения одинакова для всех элементов в массиве.
miex 03

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

Я отсортировал список по-другому и положил Кико в кровать Лизы; просто для удовольствия и посмотрите, какие изменения будут внесены ...

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

поэтому мой ответ: используйте массив в своей функции обратного вызова это очень похоже на ответ Дэна Тао , я просто забываю соединение (возможно, потому, что я удалил массив массивов уникального элемента :))
Используя вашу структуру данных, тогда он было бы :

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

и тестовая нагрузка была бы интересной ...


Серьезно, вот и ответ
Радек

7

Ни один из этих ответов не является идеальным в качестве универсального метода для использования нескольких полей при сортировке. Все вышеперечисленные подходы неэффективны, так как они либо требуют сортировки массива несколько раз (что в достаточно большом списке может сильно замедлить работу), либо генерируют огромное количество мусорных объектов, которые виртуальной машине нужно будет очистить (и, в конечном итоге, замедлить программа выключена).

Вот решение, которое является быстрым, эффективным, легко допускает обратную сортировку и может использоваться с underscoreили 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 } ]

Причина, по которой я предпочитаю этот метод, заключается в том, что он позволяет выполнять быструю сортировку по произвольному количеству полей, не генерирует много мусора и не выполняет конкатенацию строк внутри сортировки и может легко использоваться, чтобы некоторые столбцы были отсортированы в обратном порядке, в то время как столбцы порядка используют естественный Сортировать.



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/


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.
Майк К.

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.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.