Guión bajo: sortBy () basado en múltiples atributos

115

Estoy tratando de ordenar una matriz con objetos basados ​​en múltiples atributos. Es decir, si el primer atributo es el mismo entre dos objetos, se debe utilizar un segundo atributo para comparar los dos objetos. Por ejemplo, considere la siguiente matriz:

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

Ordenándolos por el roomNumberatributo, usaría el siguiente código:

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

Esto funciona bien, pero ¿cómo procedo para que 'John' y 'Lisa' se clasifiquen correctamente?

Cristiano r
fuente

Respuestas:

250

sortBy dice que es un algoritmo de ordenación estable, por lo que debería poder ordenar primero por su segunda propiedad, luego ordenar nuevamente por su primera propiedad, así:

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

Cuando el segundo sortBydescubre que John y Lisa tienen el mismo número de habitación, los mantendrá en el orden en que los encontró, que el primero sortBypuso en "Lisa, John".

Rory MacLeod
fuente
12
Hay una publicación de blog que amplía esto e incluye buena información sobre la clasificación de propiedades ascendentes y descendentes.
Alex C
4
Aquí se puede encontrar una solución más simple para el tipo encadenado . Para ser justos, parece que la publicación del blog se escribió después de que se dieron estas respuestas, pero me ayudó a resolver esto después de intentar usar el código en la respuesta anterior y fallar.
Mike Devenney
1
¿Está seguro de que el paciente [0] .name y el paciente [1] .roomNumber deben tener el índice allí? el paciente no es una matriz ...
StinkyCat
El [0]indexador es necesario porque en el ejemplo original patientses una matriz de matrices. Esta es también la razón por la que la "solución más simple" en la publicación del blog mencionada en otro comentario no funcionará aquí.
Rory MacLeod
1
@ac_fire Aquí hay un archivo de ese enlace ahora muerto: archive.is/tiatQ
lustig
52

Aquí hay un truco hacky que a veces uso en estos casos: combine las propiedades de tal manera que el resultado sea ordenable:

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

Sin embargo, como dije, eso es bastante complicado. Para hacer esto correctamente, probablemente desee utilizar el sortmétodo principal de 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;
}

Por supuesto, esto ordenará su matriz en su lugar. Si desea una copia ordenada (como _.sortByle daría), primero clone la matriz:

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

Por aburrimiento, acabo de escribir una solución general (para ordenar por cualquier número arbitrario de claves) para esto también: eche un vistazo .

Dan Tao
fuente
Muchas gracias por esta solución terminé usando la segunda ya que mis atributos podían ser tanto cadenas como números. Entonces, ¿no parece haber una forma nativa simple de ordenar matrices?
Christian R
3
¿Por qué no es return [patient[0].roomNumber, patient[0].name];suficiente sin el join?
Csaba Toth
1
El enlace a su solución general parece estar roto (o quizás no puedo acceder a él a través de nuestro servidor proxy). ¿Podrías publicarlo aquí?
Zev Spitz
Además, ¿cómo comparelos valores de identificador que no son valores primitivos - undefined, nullu objetos lisos?
Zev Spitz
Para su información, este truco solo funciona si se asegura de que la longitud de str de cada valor sea la misma para todos los elementos de la matriz.
miex
32

Sé que llego tarde a la fiesta, pero quería agregar esto para aquellos que necesitan una solución más limpia y rápida que los que ya sugirieron. Puede encadenar las llamadas sortBy en el orden de la propiedad menos importante a la propiedad más importante. En el siguiente código, creo una nueva matriz de pacientes ordenados por Nombre dentro RoomNumber de la matriz original llamada pacientes .

var sortedPatients = _.chain(patients)
  .sortBy('Name')
  .sortBy('RoomNumber')
  .value();
Mike Devenney
fuente
4
Aunque llegues tarde, todavía tienes razón :) ¡Gracias!
Allan Jikamu
3
Agradable, muy limpio.
Jason Turan
11

por cierto, su inicializador para pacientes es un poco extraño, ¿no? ¿Por qué no inicializa esta variable como esta? Como una verdadera matriz de objetos, puede hacerlo usando _.flatten () y no como una matriz de matrices de un solo objeto, tal vez sea un problema de error tipográfico):

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

Ordené la lista de manera diferente y agregué a Kiko a la cama de Lisa; solo por diversión y ver qué cambios se harían ...

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

inspeccione ordenado y verá esto

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

entonces mi respuesta es: use una matriz en su función de devolución de llamada, esto es bastante similar a la respuesta de Dan Tao , simplemente olvido la combinación (tal vez porque eliminé la matriz de matrices de elementos únicos :))
Usando su estructura de datos, entonces sería :

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

y una carga de prueba sería interesante ...

zobidafly
fuente
En serio, esa es la respuesta
Radek Duchoň
7

Ninguna de estas respuestas es ideal como método de propósito general para usar varios campos en una clasificación. Todos los enfoques anteriores son ineficaces ya que requieren ordenar la matriz varias veces (lo que, en una lista lo suficientemente grande, podría ralentizar mucho las cosas) o generan grandes cantidades de objetos basura que la máquina virtual necesitará limpiar (y, en última instancia, ralentizar el programa abajo).

Aquí hay una solución que es rápida, eficiente, permite fácilmente la clasificación inversa y se puede utilizar con underscoreolodash , o directamente conArray.sort

La parte más importante es el compositeComparatormétodo, que toma una matriz de funciones de comparación y devuelve una nueva función de comparación compuesta.

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

También necesitará una función de comparación para comparar los campos por los que desea ordenar. La naturalSortfunción creará un comparador dado un campo en particular. Escribir un comparador para la clasificación inversa también es 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;
  }
}

(Todo el código hasta ahora es reutilizable y podría guardarse en el módulo de utilidad, por ejemplo)

A continuación, debe crear el comparador compuesto. Para nuestro ejemplo, se vería así:

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

Esto ordenará por número de habitación, seguido del nombre. Agregar criterios de clasificación adicionales es trivial y no afecta el rendimiento de la clasificación.

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

Devuelve lo siguiente

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

La razón por la que prefiero este método es que permite una clasificación rápida en un número arbitrario de campos, no genera mucha basura ni realiza una concatenación de cadenas dentro de la clasificación y se puede usar fácilmente para que algunas columnas se clasifiquen al revés mientras que las columnas de orden usan natural ordenar.

Andrew Newdigate
fuente
2

Quizás underscore.js o simplemente los motores de Javascript son diferentes ahora que cuando se escribieron estas respuestas, pero pude resolver esto simplemente devolviendo una matriz de las claves de clasificación.

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

En acción, vea este violín: https://jsfiddle.net/mikeular/xenu3u91/

Mike K
fuente
2

Solo devuelve una matriz de propiedades que desea ordenar:

Sintaxis de ES6

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

Sintaxis de ES5

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

Esto no tiene los efectos secundarios de convertir un número en una cadena.

Suerte Soni
fuente
1

Puede concatenar las propiedades por las que desea ordenar en el iterador:

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

o algo equivalente.

NOTA: Dado que está convirtiendo el atributo numérico roomNumber en una cadena, tendría que hacer algo si tuviera números de habitación> 10. De lo contrario, 11 vendrá antes que 2. Puede rellenar con ceros a la izquierda para resolver el problema, es decir, 01 en lugar de 1.

Mark Sherretta
fuente
1

Creo que será mejor que uses en _.orderBylugar de sortBy:

_.orderBy(patients, ['name', 'roomNumber'], ['asc', 'desc'])
ZhangYi
fuente
4
¿Estás seguro de que orderBy está subrayado? No puedo verlo en los documentos o en mi archivo .d.ts.
Zachary Dow
1
No hay orderBy en el subrayado.
AfroMogli
1
_.orderByfunciona, pero es un método de la biblioteca lodash, no de subrayado: lodash.com/docs/4.17.4#orderBy lodash es principalmente un reemplazo directo del subrayado, por lo que podría ser apropiado para el OP.
Mike K
0

Si está usando Angular, puede usar su filtro numérico en el archivo html en lugar de agregar cualquier controlador JS o CSS. Por ejemplo:

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

En ese ejemplo, si val = 1234567, se mostrará como

  No fractions: 1,234,567

Ejemplo y orientación adicional en: https://docs.angularjs.org/api/ng/filter/number

junktrunk
fuente