Подчеркивание: sortBy() на основе нескольких атрибутов

Я пытаюсь сортировать массив с объектами на основе нескольких атрибутов. I. Если первый атрибут совпадает между двумя объектами, для совместного использования двух объектов должен использоваться второй атрибут. Например, рассмотрим следующий массив:

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

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

Ответ 1

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

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

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

Ответ 2

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

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

Из скуки я просто написал общее решение (для сортировки любым произвольным числом ключей): посмотреть.

Ответ 3

Я знаю, что опаздываю на вечеринку, но я хотел добавить это для тех, кто нуждается в чистом и быстрейшем решении, которое уже было предложено. Вы можете связать вызовы sortBy в порядке наименее важного свойства с наиболее важным свойством. В приведенном ниже коде я создаю новый массив пациентов, отсортированных по имени в RoomNumber из исходного массива, называемого пациентами.

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

Ответ 4

Кстати, ваш инициализатор для пациентов немного странный, не так ли? почему бы вам не инициализировать эту переменную как это - как истинный массив объектов - вы можете сделать это с помощью _. 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}
]

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

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

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

Ответ 5

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

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

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

Ответ 7

Возможно, 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/

Ответ 8

Вы можете объединить свойства, которые хотите отсортировать, в итераторе:

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

или что-то подобное.

ПРИМЕЧАНИЕ. Поскольку вы преобразовываете числовой атрибут roomNumber в строку, вам нужно будет что-то сделать, если у вас есть номера номеров > 10. В противном случае 11 придет раньше 2. Вы можете использовать начальные нули для решения проблемы, т.е. 01 вместо 1.

Ответ 9

Я думаю, вам лучше использовать _.orderBy вместо sortBy:

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

Ответ 10

Просто верните массив свойств, с которыми вы хотите отсортировать:

Синтаксис 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]
})

Это не имеет никаких побочных эффектов от преобразования числа в строку.

Ответ 11

Если вы используете 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.