Підкреслення: 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
Існує допис у блозі, який розширює це питання та містить добру інформацію про сортування за зростанням та низхідними властивостями.
Алекс C

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

1
Ви впевнені, що пацієнт [0]. Ім'я та пацієнт [1] .roomNumber повинні мати там індекс? пацієнт - це не масив ...
StinkyCat

[0]Індексатор потрібно тому , що в початковому прикладі, patientsявляє собою масив з масивів. Ось чому "простіше рішення" в публікації блогу, згаданому в іншому коментарі, тут не працюватиме.
Rory MacLeod

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

З нудьги я просто написав загальне рішення (для сортування за будь-якою довільною кількістю клавіш) і для цього: подивіться .


Велике спасибі за це рішення в кінцевому підсумку використовую друге, оскільки мої атрибути можуть бути як рядками, так і номерами. Тож, здається, не існує простого рідного способу сортування масивів?
Крістіан R

3
Чому просто не return [patient[0].roomNumber, patient[0].name];достатньо без join?
Csaba Toth

1
Здається, посилання на ваше загальне рішення порушено (або, можливо, я не можу отримати доступ до нього через наш проксі-сервер). Чи можете ви опублікувати його тут?
Зев Шпіц

Крім того, як compareобробляє значення, які не є примітивними значеннями undefined, nullабо звичайними об'єктами?
Зев Шпіц

FYI цей злом працює лише в тому випадку, якщо ви гарантуєте, що довжина рядка кожного значення однакова для всіх елементів масиву.
miex

32

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

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

4
Незважаючи на те, що ти спізнився, ти все ще маєш право :) Дякую!
Аллан Джікаму

3
Приємно, дуже чисто.
Джейсон Туран

11

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

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

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

Жоден із цих відповідей не є ідеальним як метод загального призначення для використання декількох полів у своєму роді. Усі вищезазначені підходи неефективні, оскільки вони або вимагають сортування масиву декілька разів (що за достатньо великим списком може значно уповільнити роботу), або вони генерують величезну кількість об'єктів для сміття, які 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 } ]

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



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
Немає порядку, підкреслюючи.
AfroMogli

1
_.orderByпрацює, але це метод бібліотеки lodash, а не підкреслення: lodash.com/docs/4.17.4#orderBy lodash - це в основному заміна для підкреслення, тому це може бути доречним для ОП.
Майк К

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.