Unterstrich: sortBy () basierend auf mehreren Attributen

115

Ich versuche, ein Array mit Objekten basierend auf mehreren Attributen zu sortieren. Das heißt, wenn das erste Attribut zwischen zwei Objekten gleich ist, sollte ein zweites Attribut verwendet werden, um die beiden Objekte zu kombinieren. Betrachten Sie beispielsweise das folgende Array:

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

Wenn roomNumberich diese nach dem Attribut sortiere, würde ich den folgenden Code verwenden:

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

Das funktioniert gut, aber wie gehe ich vor, damit 'John' und 'Lisa' richtig sortiert werden?

Christian R.
quelle

Antworten:

250

sortBy sagt, dass es sich um einen stabilen Sortieralgorithmus handelt, sodass Sie in der Lage sein sollten, zuerst nach Ihrer zweiten Eigenschaft und dann erneut nach Ihrer ersten Eigenschaft zu sortieren:

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

Wenn der zweite sortByfeststellt, dass John und Lisa dieselbe Zimmernummer haben, werden sie in der Reihenfolge gehalten, in der sie gefunden wurden. Der erste sortBySatz lautet "Lisa, John".

Rory MacLeod
quelle
12
Es gibt einen Blog-Beitrag , der dies erweitert und gute Informationen zum Sortieren aufsteigender und absteigender Eigenschaften enthält.
Alex C
4
Eine einfachere Lösung für die verkettete Sortierung finden Sie hier . Um fair zu sein, es sieht so aus, als ob der Blog-Beitrag geschrieben wurde, nachdem diese Antworten gegeben wurden, aber es hat mir geholfen, dies herauszufinden, nachdem ich versucht habe, den Code in der obigen Antwort zu verwenden, und dies fehlgeschlagen ist.
Mike Devenney
1
Sind Sie sicher, dass der Patient [0] .name und der Patient [1] .roomNumber dort den Index haben sollten? Patient ist kein Array ...
StinkyCat
Der [0]Indexer ist erforderlich, da es sich im ursprünglichen Beispiel patientsum ein Array von Arrays handelt. Dies ist auch der Grund, warum die "einfachere Lösung" in dem in einem anderen Kommentar erwähnten Blog-Beitrag hier nicht funktioniert.
Rory MacLeod
1
@ac_fire Hier ist ein Archiv dieses jetzt toten Links: archive.is/tiatQ
lustig
52

Hier ist ein hackiger Trick, den ich manchmal in diesen Fällen verwende: Kombinieren Sie die Eigenschaften so, dass das Ergebnis sortierbar ist:

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

Wie gesagt, das ist ziemlich hackig. Um dies richtig zu machen, möchten Sie wahrscheinlich die JavaScript- sortKernmethode verwenden :

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

Natürlich dies wird Ihr Array an Ort und Stelle sortieren. Wenn Sie eine sortierte Kopie wünschen (wie _.sortBySie es gerne hätten), klonen Sie zuerst das Array:

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

Aus Langeweile habe ich auch hier eine allgemeine Lösung geschrieben (um nach einer beliebigen Anzahl von Schlüsseln zu sortieren): Schauen Sie mal rein .

Dan Tao
quelle
Vielen Dank, dass diese Lösung letztendlich die zweite verwendet hat, da meine Attribute sowohl Zeichenfolgen als auch Zahlen sein können. Es scheint also keine einfache native Methode zum Sortieren von Arrays zu geben?
Christian R
3
Warum ist return [patient[0].roomNumber, patient[0].name];ohne das nicht genug join?
Csaba Toth
1
Der Link zu Ihrer allgemeinen Lösung scheint unterbrochen zu sein (oder ich kann möglicherweise nicht über unseren Proxyserver darauf zugreifen). Könnten Sie es bitte hier posten?
Zev Spitz
Auch wie funktioniert compareHandle Werte, die nicht primitive Werte - undefined, nulloder einfache Objekte?
Zev Spitz
Zu Ihrer Information, dieser Hack funktioniert nur, wenn Sie sicherstellen, dass die str-Länge jedes Werts für alle Elemente im Array gleich ist.
Miex
32

Ich weiß, dass ich zu spät zur Party komme, aber ich wollte dies für diejenigen hinzufügen, die eine sauberere und schnellere Lösung benötigen, als die bereits vorgeschlagenen. Sie können sortBy-Aufrufe in der Reihenfolge der am wenigsten wichtigen Eigenschaft mit der wichtigsten Eigenschaft verketten. Im folgenden Code erstelle ich ein neues Array von Patienten, sortiert nach Name in RoomNumber, aus dem ursprünglichen Array namens Patienten .

var sortedPatients = _.chain(patients)
  .sortBy('Name')
  .sortBy('RoomNumber')
  .value();
Mike Devenney
quelle
4
Auch wenn Sie zu spät sind, haben Sie immer noch Recht :) Danke!
Allan Jikamu
3
Schön, sehr sauber.
Jason Turan
11

Übrigens ist Ihr Initialisierer für Patienten ein bisschen komisch, nicht wahr? Warum initialisieren Sie diese Variable nicht als solche - als echtes Array von Objekten - Sie können dies mit _.flatten () tun und nicht als Array von Arrays einzelner Objekte, möglicherweise ist es ein Tippfehler):

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

Ich sortierte die Liste anders und fügte Kiko in Lisas Bett hinzu. Nur zum Spaß und sehen, welche Änderungen vorgenommen werden ...

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

inspizieren sortiert und Sie werden dies sehen

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

Meine Antwort lautet also: Verwenden Sie ein Array in Ihrer Rückruffunktion. Dies ist der Antwort von Dan Tao ziemlich ähnlich. Ich vergesse nur den Join (möglicherweise, weil ich das Array von Arrays mit einem eindeutigen Element entfernt habe :)).
Verwenden Sie dann Ihre Datenstruktur wäre :

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

und eine Testladung wäre interessant ...

zobidafly
quelle
Im Ernst, das ist die Antwort
Radek Duchoň
7

Keine dieser Antworten ist ideal als Allzweckmethode für die Verwendung mehrerer Felder in einer Sortierung. Alle oben genannten Ansätze sind ineffizient, da sie entweder das Sortieren des Arrays mehrmals erfordern (was auf einer ausreichend großen Liste die Dinge erheblich verlangsamen kann) oder große Mengen an Müllobjekten generieren, die die VM bereinigen (und letztendlich verlangsamen muss) das Programm runter).

Hier ist eine Lösung, die schnell und effizient ist, eine einfache umgekehrte Sortierung ermöglicht und mit underscoreoder lodashoder direkt mit verwendet werden kannArray.sort

Der wichtigste Teil ist die compositeComparatorMethode, die ein Array von Komparatorfunktionen verwendet und eine neue zusammengesetzte Komparatorfunktion zurückgibt.

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

Sie benötigen außerdem eine Komparatorfunktion, um die Felder zu vergleichen, nach denen Sie sortieren möchten. Die naturalSortFunktion erstellt einen Komparator für ein bestimmtes Feld. Das Schreiben eines Komparators für die umgekehrte Sortierung ist ebenfalls trivial.

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

(Der gesamte bisherige Code ist wiederverwendbar und kann beispielsweise im Dienstprogrammmodul gespeichert werden.)

Als Nächstes müssen Sie den zusammengesetzten Komparator erstellen. In unserem Beispiel würde es so aussehen:

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

Dies wird nach Raumnummer sortiert, gefolgt von Name. Das Hinzufügen zusätzlicher Sortierkriterien ist trivial und hat keinen Einfluss auf die Leistung der Sortierung.

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

Gibt Folgendes zurück

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

Der Grund, warum ich diese Methode bevorzuge, ist, dass sie eine schnelle Sortierung nach einer beliebigen Anzahl von Feldern ermöglicht, nicht viel Müll erzeugt oder eine Zeichenfolgenverkettung innerhalb der Sortierung durchführt und einfach verwendet werden kann, sodass einige Spalten umgekehrt sortiert werden, während Auftragsspalten natürlich verwendet werden Sortieren.

Andrew Newdigate
quelle
2

Vielleicht unterscheiden sich underscore.js oder nur Javascript-Engines jetzt von denen, als diese Antworten geschrieben wurden, aber ich konnte dies lösen, indem ich nur ein Array der Sortierschlüssel zurückgab.

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

In Aktion sehen Sie bitte diese Geige: https://jsfiddle.net/mikeular/xenu3u91/

Mike K.
quelle
2

Nur ein Array von Eigenschaften , mit denen Sie sortieren möchten:

ES6-Syntax

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

ES5-Syntax

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

Dies hat keine Nebenwirkungen beim Konvertieren einer Zahl in eine Zeichenfolge.

Glückliche Soni
quelle
1

Sie können die Eigenschaften, nach denen Sie sortieren möchten, im Iterator verketten:

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

oder etwas Äquivalentes.

HINWEIS: Da Sie das numerische Attribut roomNumber in eine Zeichenfolge konvertieren, müssten Sie etwas tun, wenn Sie Raumnummern> 10 hätten. Andernfalls wird 11 vor 2 stehen. Sie können mit führenden Nullen auffüllen, um das Problem zu lösen, dh 01 anstelle von 01 1.

Mark Sherretta
quelle
1

Ich denke, Sie sollten besser verwenden _.orderByals sortBy:

_.orderBy(patients, ['name', 'roomNumber'], ['asc', 'desc'])
ZhangYi
quelle
4
Sind Sie sicher, dass orderBy unterstrichen ist? Ich kann es nicht in den Dokumenten oder in meiner .d.ts-Datei sehen.
Zachary Dow
1
Der Unterstrich enthält keine Bestellung.
AfroMogli
1
_.orderByfunktioniert, aber es ist eine Methode der lodash-Bibliothek, kein Unterstrich: lodash.com/docs/4.17.4#orderBy lodash ist meistens ein Ersatz für Unterstriche, daher ist es möglicherweise für das OP geeignet.
Mike K
0

Wenn Sie Angular verwenden, können Sie den Nummernfilter in der HTML-Datei verwenden, anstatt JS- oder CSS-Handler hinzuzufügen. Beispielsweise:

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

In diesem Beispiel wird val = 1234567 als angezeigt

  No fractions: 1,234,567

Beispiel und weitere Anleitung unter: https://docs.angularjs.org/api/ng/filter/number

junktrunk
quelle