Underscore: sortBy () basato su più attributi

Sto cercando di ordinare un array con oggetti basati su più attributi. Ad esempio, se il primo attributo è uguale tra due oggetti, è necessario utilizzare un secondo attributo per eseguire il confronto tra i due oggetti. Ad esempio, considera il seguente 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}] ]; 

Ordinando questi per l’attributo roomNumber il seguente codice:

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

Funziona bene, ma come procedo in modo che ‘John’ e ‘Lisa’ siano ordinati correttamente?

sortBy dice che è un algoritmo di ordinamento stabile quindi dovresti essere in grado di ordinare prima la tua seconda proprietà, quindi ordinare di nuovo con la tua prima proprietà, in questo modo:

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

Quando il secondo sortBy scopre che John e Lisa hanno lo stesso numero di stanza, li manterrà nell’ordine in cui li ha trovati, quale il primo sortBy Impostato su “Lisa, John”.

Ecco un trucco hacky che a volte uso in questi casi: combina le proprietà in modo tale che il risultato sia ordinabile:

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

Tuttavia, come ho detto, è piuttosto hacky. Per farlo correttamente, probabilmente vorrai utilizzare il metodo di sort JavaScript principale :

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

Ovviamente, questo ordinerà il tuo array sul posto. Se vuoi una copia ordinata (come _.sortBy ti darebbe), clona prima l’array:

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

Per noia, ho appena scritto una soluzione generale (da ordinare per qualsiasi numero arbitrario di chiavi) anche per questo: date un’occhiata .

So che sono in ritardo per la festa, ma volevo aggiungere questo per chi ha bisogno di una soluzione pulita e veloce che quelli già suggeriti. È ansible concatenare ordinamento per chiamate in ordine di proprietà meno importante per la proprietà più importante. Nel codice seguente creo una nuova serie di pazienti ordinati per nome all’interno di RoomNumber dall’array originale chiamato pazienti .

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

il tuo inizializzatore per i pazienti è un po ‘strano, vero? perché non inizializzare questa variabile in questo modo -come una vera matrice di oggetti -si può fare usando _.flatten () e non come una matrice di matrici di singoli oggetti, forse è un errore di battitura):

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

Ho ordinato la lista in modo diverso e ho aggiunto Kiko nel letto di Lisa; solo per divertimento e vedere quali cambiamenti sarebbero fatti …

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

ispeziona ordinato e vedrai questo

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

quindi la mia risposta è: usare un array nella funzione di callback è abbastanza simile alla risposta di Dan Tao , ho appena dimenticato il join (forse perché ho rimosso l’array di array di item unici :))
Usando la tua struttura dati, allora sarebbe:

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

e un carico di prova sarebbe interessante …

Nessuna di queste risposte è ideale come metodo generico per l’utilizzo di più campi in un ordinamento. Tutti gli approcci di cui sopra sono inefficienti in quanto richiedono l’ordinamento dell’array più volte (che, in una lista abbastanza grande potrebbe rallentare molto) o generano enormi quantità di oggetti inutili che la VM dovrà ripulire (e in definitiva rallentando il programma verso il basso).

Ecco una soluzione che è veloce, efficiente, consente facilmente l’ordinamento inverso e può essere utilizzata con underscore o lodash , o direttamente con Array.sort

La parte più importante è il metodo compositeComparator , che accetta una serie di funzioni di confronto e restituisce una nuova funzione di comparatore composito.

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

Avrai anche bisogno di una funzione di confronto per confrontare i campi che desideri ordinare. La funzione naturalSort creerà un comparatore dato un particolare campo. Anche scrivere un comparatore per l’ordinamento inverso è banale.

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

(Tutto il codice finora è riutilizzabile e potrebbe essere tenuto in un modulo di utilità, ad esempio)

Successivamente, è necessario creare il comparatore composito. Per il nostro esempio, sarebbe simile a questo:

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

Questo ordinerà per numero di stanza, seguito dal nome. L'aggiunta di ulteriori criteri di ordinamento è banale e non influisce sulle prestazioni dell'ordinamento.

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

Restituisce il seguente

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

Il motivo per cui preferisco questo metodo è che consente di eseguire rapidamente l'ordinamento su un numero arbitrario di campi, non genera molto garbage o esegue concatenazioni di stringhe all'interno dell'ordinamento e può essere facilmente utilizzato in modo che alcune colonne vengano invertite ordinate mentre le colonne di ordine utilizzano il naturale ordinare.

Semplice esempio da http://janetriley.net/2014/12/sort-on-multiple-keys-with-underscores-sortby.html (per gentile concessione di @MikeDevenney)

Codice

 var FullySortedArray = _.sortBy(( _.sortBy(array, 'second')), 'first'); 

Con i tuoi dati

 var FullySortedArray = _.sortBy(( _.sortBy(patients, 'roomNumber')), 'name'); 

Forse underscore.js o solo i motori Javascript sono diversi ora rispetto a quando sono state scritte queste risposte, ma sono stato in grado di risolvere questo problema semplicemente restituendo un array delle chiavi di ordinamento.

 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 [ob, oa]; }); // output is now sorted by b ascending, a ascending 

In azione, per favore vedi questo violino: https://jsfiddle.net/mikeular/xenu3u91/

È ansible concatenare le proprietà da ordinare nell’iteratore:

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

o qualcosa di equivalente.

NOTA: Dato che stai convertendo l’attributo numerico roomNumber in una stringa, dovresti fare qualcosa se tu avessi numeri di stanza> 10. Altrimenti 11 verrà prima di 2. Puoi eseguire il pad con gli zeri iniziali per risolvere il problema, ovvero 01 invece di 1.

Penso che faresti meglio a usare _.orderBy invece di sortBy :

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

Basta restituire una serie di proprietà che si desidera ordinare con:

Sintassi ES6

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

Sintassi ES5

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

Questo non ha effetti collaterali della conversione di un numero in una stringa.