Podkreślenie: sortBy () na podstawie wielu atrybutów

115

Próbuję posortować tablicę z obiektami na podstawie wielu atrybutów. To znaczy, jeśli pierwszy atrybut jest taki sam między dwoma obiektami, do porównania tych dwóch obiektów należy użyć drugiego atrybutu. Na przykład rozważmy następującą tablicę:

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

Sortując je według roomNumberatrybutu, użyłbym następującego kodu:

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

Działa to dobrze, ale jak mam postępować, aby „John” i „Lisa” zostały odpowiednio posortowane?

Christian R.
źródło

Odpowiedzi:

250

sortBy mówi, że jest to stabilny algorytm sortowania, więc najpierw powinieneś móc sortować według drugiej właściwości, a następnie sortować ponownie według pierwszej właściwości, na przykład:

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

Gdy druga osoba sortBystwierdzi, że John i Lisa mają ten sam numer pokoju, zachowa ich w kolejności, w jakiej ich znaleźli, czyli w pierwszej sortByustawionej na „Lisa, John”.

Rory MacLeod
źródło
12
Jest post na blogu, który rozszerza ten temat i zawiera dobre informacje na temat sortowania rosnąco i malejąco właściwości.
Alex C
4
Prostsze rozwiązanie sortowania łańcuchowego można znaleźć tutaj . Szczerze mówiąc, wygląda na to, że post na blogu został napisany po udzieleniu tych odpowiedzi, ale pomógł mi to zrozumieć po próbie użycia kodu w powyższej odpowiedzi i niepowodzeniu.
Mike Devenney
1
Jesteś pewien, że pacjent [0] .name i pacjent [1] .roomNumber powinny mieć tam indeks? pacjent nie jest tablicą ...
StinkyCat
[0]Podziałowy jest wymagane, ponieważ w przykładzie początkowym, patientsjest tablicą tablic. Dlatego też „prostsze rozwiązanie” w poście na blogu, o którym mowa w innym komentarzu, nie będzie tutaj działać.
Rory MacLeod
1
@ac_fire Oto archiwum tego teraz martwego linku: archive.is/tiatQ
lustig
52

Oto hacky trik, którego czasami używam w takich przypadkach: połącz właściwości w taki sposób, aby wynik był możliwy do sortowania:

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

Jednak, jak powiedziałem, jest to dość hakerskie. Aby zrobić to poprawnie, prawdopodobnie chciałbyś faktycznie użyć podstawowej sortmetody 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;
}

Oczywiście, to będzie uporządkować swoją tablicę w miejscu. Jeśli chcesz posortowaną kopię (tak jak _.sortByby ci to dało), najpierw sklonuj tablicę:

function sortOutOfPlace(sequence, sorter) {
  var copy = _.clone(sequence);
  copy.sort(sorter);
  return copy;
}

Z nudów właśnie napisałem ogólne rozwiązanie (aby posortować według dowolnej liczby kluczy) również na to: spójrz .

Dan Tao
źródło
Wielkie dzięki za to rozwiązanie skończyło się na korzystaniu z drugiego, ponieważ moimi atrybutami mogą być zarówno łańcuchy, jak i liczby. Więc wydaje się, że nie ma prostego natywnego sposobu sortowania tablic?
Christian R
3
Dlaczego nie return [patient[0].roomNumber, patient[0].name];wystarczy bez join?
Csaba Toth
1
Wydaje się, że łącze do ogólnego rozwiązania jest uszkodzone (lub być może nie mogę uzyskać do niego dostępu przez nasz serwer proxy). Czy mógłbyś zamieścić to tutaj?
Zev Spitz
Również, w jaki sposób comparewartości uchwytów, które nie są prymitywne - wartości undefined, nulllub zwykły obiekty?
Zev Spitz,
FYI ten hack działa tylko wtedy, gdy upewnisz się, że długość ciągu każdej wartości jest taka sama dla wszystkich elementów w tablicy.
miex
32

Wiem, że spóźniłem się na imprezę, ale chciałem to dodać dla tych, którzy potrzebują czystszego i szybszego rozwiązania, które już sugerowali. Wywołania sortBy można łączyć w kolejności od najmniej ważnej właściwości do najważniejszej właściwości. W poniższym kodzie tworzę nową tablicę pacjentów posortowaną według nazwy w RoomNumber z oryginalnej tablicy o nazwie pacjenci .

var sortedPatients = _.chain(patients)
  .sortBy('Name')
  .sortBy('RoomNumber')
  .value();
Mike Devenney
źródło
4
Nawet jeśli się spóźniłeś, nadal masz rację :) Dzięki!
Allan Jikamu
3
Ładnie, bardzo czysto.
Jason Turan
11

przy okazji twój inicjator dla pacjentów jest trochę dziwny, prawda? dlaczego nie zainicjujesz tej zmiennej jako - jako prawdziwej tablicy obiektów - możesz to zrobić za pomocą _.flatten (), a nie jako tablicę tablic pojedynczego obiektu, może to błąd literówki):

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

Posortowałem listę inaczej i dodałem Kiko do łóżka Lisy; tylko dla zabawy i zobacz, jakie zmiany zostaną wprowadzone ...

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

sprawdź posortowane, a zobaczysz to

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

więc moja odpowiedź brzmi: użyj tablicy w swojej funkcji zwrotnej jest to dość podobne do odpowiedzi Dana Tao , po prostu zapomniałem o złączeniu (może dlatego, że usunąłem tablicę tablic unikalnych pozycji :))
Używając struktury danych, to byłoby :

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

a ładowanie testowe byłoby interesujące ...

zobidafly
źródło
Poważnie, taka jest odpowiedź
Radek Duchoň
7

Żadna z tych odpowiedzi nie jest idealna jako metoda ogólnego przeznaczenia do korzystania z wielu pól w sortowaniu. Wszystkie powyższe podejścia są nieefektywne, ponieważ wymagają wielokrotnego sortowania tablicy (co na wystarczająco dużej liście może spowolnić działanie) lub generują ogromne ilości śmieci, które maszyna wirtualna będzie musiała wyczyścić (i ostatecznie spowolnić program wyłączony).

Oto rozwiązanie, które jest szybkie, wydajne, łatwo umożliwia odwrotne sortowanie i może być używane z underscorelub lodashlub bezpośrednio zArray.sort

Najważniejszą częścią jest compositeComparatormetoda, która przyjmuje tablicę funkcji komparatora i zwraca nową funkcję komparatora złożonego.

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

Będziesz także potrzebować funkcji komparatora do porównywania pól, według których chcesz sortować. naturalSortFunkcja stworzy komparator podana konkretna dziedzina. Napisanie komparatora do sortowania wstecznego jest również trywialne.

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

(Cały dotychczasowy kod jest wielokrotnego użytku i może być przechowywany na przykład w module narzędziowym)

Następnie musisz utworzyć komparator złożony. W naszym przykładzie wyglądałoby to tak:

var cmp = compositeComparator([naturalSort('roomNumber'), naturalSort('name')]);

Spowoduje to posortowanie według numeru pokoju, po którym następuje nazwa. Dodanie dodatkowych kryteriów sortowania jest trywialne i nie wpływa na wydajność sortowania.

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

Zwraca następujące dane

[ { name: 'Chris', roomNumber: 1, bedNumber: 1 },
  { name: 'Lisa', roomNumber: 2, bedNumber: 2 },
  { name: 'Omar', roomNumber: 2, bedNumber: 1 },
  { name: 'John', roomNumber: 3, bedNumber: 1 } ]

Powodem, dla którego wolę tę metodę jest to, że pozwala ona na szybkie sortowanie na dowolnej liczbie pól, nie generuje dużo śmieci ani nie wykonuje konkatenacji ciągów wewnątrz sortowania i może być łatwo używana, aby niektóre kolumny były sortowane odwrotnie, podczas gdy kolumny kolejności używają naturalnych sortować.

Andrew Newdigate
źródło
2

Być może underscore.js lub po prostu silniki Javascript są teraz inne niż wtedy, gdy pisano te odpowiedzi, ale udało mi się to rozwiązać, zwracając po prostu tablicę kluczy sortowania.

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

W akcji zobacz to skrzypce: https://jsfiddle.net/mikeular/xenu3u91/

Mike K.
źródło
2

Po prostu zwróć tablicę właściwości, według których chcesz posortować:

Składnia ES6

var sortedArray = _.sortBy(patients, patient => [patient[0].name, patient[1].roomNumber])

Składnia ES5

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

Nie ma to żadnych skutków ubocznych konwersji liczby na ciąg.

Lucky Soni
źródło
1

Możesz połączyć właściwości, według których chcesz sortować w iteratorze:

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

lub coś równoważnego.

UWAGA: Ponieważ konwertujesz atrybut numeryczny roomNumber na łańcuch, musiałbyś coś zrobić, gdybyś miał numery pokoi> 10. W przeciwnym razie 11 wystąpi przed 2. Aby rozwiązać problem, możesz wypełnić zera wiodące, np. 01 zamiast 1.

Mark Sherretta
źródło
1

Myślę, że lepiej użyj _.orderByzamiast sortBy:

_.orderBy(patients, ['name', 'roomNumber'], ['asc', 'desc'])
ZhangYi
źródło
4
Czy na pewno orderBy jest w podkreśleniu? Nie widzę tego w dokumentach ani w moim pliku .d.ts.
Zachary Dow
1
W podkreśleniu nie ma uporządkowania według.
AfroMogli
1
_.orderBydziała, ale jest to metoda biblioteki lodash, a nie podkreślenia: lodash.com/docs/4.17.4#order Przez lodash jest głównie zastępczym zamiennikiem podkreślenia, więc może być odpowiedni dla OP.
Mike K
0

Jeśli używasz Angulara, możesz użyć jego filtra liczbowego w pliku html zamiast dodawać jakiekolwiek moduły obsługi JS lub CSS. Na przykład:

  No fractions: <span>{{val | number:0}}</span><br>

W tym przykładzie, jeśli val = 1234567, zostanie wyświetlony jako

  No fractions: 1,234,567

Przykład i dalsze wskazówki na: https://docs.angularjs.org/api/ng/filter/number

junktrunk
źródło