Sottolineato: sortBy () basato su più attributi


115

Sto cercando di ordinare un array con oggetti in base a più attributi. Cioè se il primo attributo è lo stesso tra due oggetti un secondo attributo dovrebbe essere usato per confrontare 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}]
               ];

Ordinandoli per l' roomNumberattributo utilizzerei il seguente codice:

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

Funziona bene, ma come procedo in modo che "John" e "Lisa" vengano ordinati correttamente?

Risposte:


250

sortBy dice che si tratta di un algoritmo di ordinamento stabile, quindi dovresti essere in grado di ordinare prima in base alla tua seconda proprietà, quindi ordinare di nuovo in base alla tua prima proprietà, in questo modo:

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

Quando il secondo sortByscopre che John e Lisa hanno lo stesso numero di stanza, li manterrà nell'ordine in cui li ha trovati, il primo sortByimpostato su "Lisa, John".


12
C'è un post sul blog che si espande su questo e include buone informazioni sull'ordinamento delle proprietà ascendenti e discendenti.
Alex C

4
Una soluzione più semplice per l'ordinamento concatenato può essere trovata qui . Per essere onesti, sembra che il post sul blog sia stato scritto dopo che sono state fornite queste risposte, ma mi ha aiutato a capirlo dopo aver provato a utilizzare il codice nella risposta sopra e senza successo.
Mike Devenney

1
Sei sicuro che il paziente [0] .name e il paziente [1] .roomNumber dovrebbero avere l'indice lì? il paziente non è un array ...
StinkyCat

L' [0]indicizzatore è richiesto perché nell'esempio originale patientsè un array di array. Questo è anche il motivo per cui la "soluzione più semplice" nel post del blog menzionato in un altro commento non funzionerà qui.
Rory MacLeod

1
@ac_fire Ecco un archivio di quel link ormai morto: archive.is/tiatQ
lustig

52

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 complicato. Per farlo correttamente, probabilmente vorrai utilizzare effettivamente il sortmetodo 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;
}

Naturalmente, questo ordinerà il tuo array in posizione. Se vuoi una copia ordinata (come _.sortByti 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 (per ordinare in base a un numero arbitrario di chiavi) anche per questo: dai un'occhiata .


Grazie mille per questa soluzione ho finito per utilizzare la seconda poiché i miei attributi potrebbero essere sia stringhe che numeri. Quindi non sembra esserci un semplice modo nativo per ordinare gli array?
Christian R

3
Perché non è return [patient[0].roomNumber, patient[0].name];abbastanza senza il join?
Csaba Toth

1
Il collegamento alla tua soluzione generale sembra essere interrotto (o forse non riesco ad accedervi tramite il nostro server proxy). Potresti pubblicarlo qui?
Zev Spitz

Inoltre, come fa comparevalori di handle che non sono valori primitivi - undefined, nullo oggetti semplici?
Zev Spitz

Cordiali saluti, questo hack funziona solo se ti assicuri che la lunghezza di ogni valore sia la stessa per tutti gli elementi nell'array.
miex

32

So di essere in ritardo alla festa, ma volevo aggiungere questo per coloro che necessitano di una soluzione più pulita e rapida che quelli già suggeriti. È possibile concatenare le chiamate sortBy in ordine dalla proprietà meno importante alla proprietà più importante. Nel codice seguente creo un nuovo array di pazienti ordinati per Nome all'interno di RoomNumber dall'array originale chiamato pazienti .

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

4
Anche se sei in ritardo hai comunque ragione :) Grazie!
Allan Jikamu

3
Bello, molto pulito.
Jason Turan

11

btw il tuo inizializzatore per i pazienti è un po 'strano, non è vero? perché non inizializzi questa variabile in questo modo -come un vero array di oggetti -puoi farlo usando _.flatten () e non come un array di array di un singolo oggetto, forse è un problema di 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 l'elenco in modo diverso e ho aggiunto Kiko nel letto di Lisa; solo per divertimento e vedere quali cambiamenti sarebbero stati 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 è: usa un array nella tua funzione di callback questo è abbastanza simile alla risposta di Dan Tao , ho solo dimenticato il join (forse perché ho rimosso l'array di array di elementi univoci :))
usando la tua struttura dati, quindi sarebbe :

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

e un testload sarebbe interessante ...


Seriamente, questa è la risposta
Radek Duchoň

7

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 (il che, su un elenco abbastanza ampio potrebbe rallentare molto) o generano enormi quantità di oggetti spazzatura che la VM dovrà pulire (e alla fine rallentando il programma inattivo).

Ecco una soluzione veloce, efficiente, che consente facilmente lo smistamento inverso e può essere utilizzata con underscoreo lodash, o direttamente conArray.sort

La parte più importante è il compositeComparatormetodo, che accetta un array di funzioni di confronto e restituisce una nuova funzione di confronto composto.

/**
 * 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 su cui desideri ordinare. La naturalSortfunzione creerà un comparatore dato un particolare campo. Anche la scrittura di 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 fino ad ora è riutilizzabile e potrebbe essere mantenuto nel modulo di utilità, per 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 quanto segue

[ { 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 l'ordinamento rapido su un numero arbitrario di campi, non genera molta spazzatura o esegue la concatenazione di stringhe all'interno dell'ordinamento e può essere facilmente utilizzato in modo che alcune colonne siano ordinate al contrario mentre le colonne dell'ordine usano il naturale ordinare.



2

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 risolverlo 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 [o.b, o.a];
});

// output is now sorted by b ascending, a ascending

In azione, guarda questo violino: https://jsfiddle.net/mikeular/xenu3u91/


2

Restituisci semplicemente un array di proprietà con cui vuoi ordinare:

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

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


1

Puoi concatenare le proprietà che vuoi ordinare nell'iteratore:

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

o qualcosa di equivalente.

NOTA: poiché stai convertendo l'attributo numerico roomNumber in una stringa, dovresti fare qualcosa se avessi numeri di stanza> 10. Altrimenti 11 verrà prima di 2. Puoi riempire gli zeri iniziali per risolvere il problema, cioè 01 invece di 1.


1

Penso che faresti meglio a usare _.orderByinvece di sortBy:

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

4
Sei sicuro che orderBy sia in sottolineatura? Non riesco a vederlo nei documenti o nel mio file .d.ts.
Zachary Dow

1
Non c'è ordine di sottolineatura.
AfroMogli

1
_.orderByfunziona, ma è un metodo della libreria lodash, non underscore: lodash.com/docs/4.17.4#orderBy lodash è principalmente un sostituto drop-in per underscore, quindi potrebbe essere appropriato per l'OP.
Mike K

0

Se ti capita di utilizzare Angular, puoi utilizzare il suo filtro numerico nel file html piuttosto che aggiungere gestori JS o CSS. Per esempio:

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

In questo esempio, se val = 1234567, verrà visualizzato come

  No fractions: 1,234,567

Esempio e ulteriori indicazioni su: https://docs.angularjs.org/api/ng/filter/number

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.