web-dev-qa-db-fra.com

Underscore: sortBy () basé sur plusieurs attributs

J'essaie de trier un tableau avec des objets basés sur plusieurs attributs. Ie si le premier attribut est le même entre deux objets, un deuxième attribut doit être utilisé pour comparer les deux objets. Par exemple, considérons le tableau suivant:

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

En les triant selon l'attribut roomNumber, j'utiliserais le code suivant:

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

Cela fonctionne bien, mais comment puis-je procéder pour que «John» et «Lisa» soient triés correctement?

102
Christian R

sortBy indique qu'il s'agit d'un algorithme de tri stable. Vous devriez donc pouvoir commencer par trier votre deuxième propriété, puis trier à nouveau votre première propriété, comme ceci:

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

Lorsque la deuxième sortBy constate que John et Lisa ont le même numéro de chambre, ils seront conservés dans l'ordre dans lequel ils les ont trouvés, ce que la première sortBy définira sur "Lisa, John".

236
Rory MacLeod

Voici un truc hacky que j’utilise parfois dans ces cas: combinez les propriétés de telle sorte que le résultat puisse être trié:

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

Cependant, comme je l'ai dit, c'est plutôt hacky. Pour le faire correctement, vous voudrez probablement utiliser la méthode JavaScript sort de base :

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

Bien sûr, this va trier votre tableau en place. Si vous voulez une copie triée (comme _.sortBy vous en donnerait), clonez d'abord le tableau:

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

Par ennui, je viens d’écrire une solution générale (trier par un nombre quelconque de clés) pour cela aussi: regardez .

49
Dan Tao

Je sais que je suis en retard au parti, mais je voulais ajouter ceci pour ceux qui ont besoin d'une solution plus propre et plus rapide que ceux déjà suggérés. Vous pouvez chaîner les appels sortBy de la propriété la moins importante à la propriété la plus importante. Dans le code ci-dessous, je crée un nouveau groupe de patients triés par Nom dans RoomNumber à partir du groupe initial appelé patients

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

btw votre initialiseur pour les patients est un peu bizarre, n'est-ce pas? pourquoi ne pas initialisez cette variable en tant que - en tant que vrai tableau d'objets - vous pouvez le faire en utilisant _.flatten () et non en tant que tableau de tableaux d'objets uniques c'est peut-être une faute de frappe):

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

Je triai la liste différemment et ajoutai Kiko dans le lit de Lisa. juste pour le plaisir et voir quels changements seraient apportés ...

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

inspecter trié et vous verrez cela

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

donc ma réponse est: utilise un tableau dans votre fonction de rappel ceci est assez similaire à la réponse de Dan Tao , j’oublie juste la jointure article :))
En utilisant votre structure de données, alors ce serait:

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

et une charge de test serait intéressante ...

10
zobidafly

Aucune de ces réponses n'est idéale comme méthode générale d'utilisation de plusieurs champs dans un même type. Toutes les approches ci-dessus sont inefficaces, car elles nécessitent soit de trier le tableau plusieurs fois (ce qui, sur une liste assez longue, pourrait ralentir considérablement les choses), soit de générer d'énormes quantités d'objets parasites que la VM devra nettoyer. (et en fin de compte ralentir le programme). 

Voici une solution rapide, efficace, qui permet facilement le tri inversé et qui peut être utilisée avec underscore ou lodash ou directement avec Array.sort

La partie la plus importante est la méthode compositeComparator, qui prend un tableau de fonctions de comparateur et retourne une nouvelle fonction de comparateur composite.

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

Vous aurez également besoin d'une fonction de comparaison pour comparer les champs que vous souhaitez trier. La fonction naturalSort créera un comparateur à partir d'un champ particulier. Écrire un comparateur pour le tri inversé est également 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;
  }
}

(Tout le code jusqu'à présent est réutilisable et pourrait être conservé dans le module utilitaire, par exemple)

Ensuite, vous devez créer le comparateur composite. Pour notre exemple, cela ressemblerait à ceci:

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

Cela va trier par numéro de chambre, suivi par nom. L'ajout de critères de tri supplémentaires est trivial et n'affecte pas les performances du tri.

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

Renvoie le suivant

[ { 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 raison pour laquelle je préfère cette méthode est qu’elle permet un tri rapide sur un nombre arbitraire de champs, ne génère pas beaucoup de déchets et n’effectue pas de concaténation de chaînes à l’intérieur du tri. Trier.

5
Andrew Newdigate

Exemple simple de http://janetriley.net/2014/12/sort-on-multiple-keys-with-underscores-sortby.html (avec la permission de @MikeDevenney)

Code

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

Avec vos données

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

Peut-être que underscore.js ou simplement les moteurs Javascript sont différents de ceux utilisés au moment de la rédaction de ces réponses, mais j’ai pu résoudre ce problème en renvoyant simplement un tableau des clés de tri.

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 action, veuillez voir ce violon: https://jsfiddle.net/mikeular/xenu3u91/

2
Mike K

Juste retournez un tableau de propriétés vous voulez trier avec:

Syntaxe ES6

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

Syntaxe ES5

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

Cela n'a pas d'effets secondaires de convertir un nombre en chaîne.

1
Lucky Soni

Vous pouvez concaténer les propriétés que vous souhaitez trier dans l'itérateur:

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

ou quelque chose d'équivalent.

REMARQUE: puisque vous convertissez l'attribut numérique roomNumber en chaîne, vous devez effectuer quelque chose si vous avez un nombre de chambres supérieur à 10. Sinon, 11 viendra avant 2. Vous pouvez ajouter des zéros au début, c’est-à-dire 01 au lieu de 1.

1
Mark Sherretta

Je pense que vous feriez mieux d'utiliser _.orderBy au lieu de sortBy:

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

Si vous utilisez Angular, vous pouvez utiliser son filtre de numéro dans le fichier html plutôt que d’ajouter des gestionnaires JS ou CSS. Par exemple:

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

Dans cet exemple, si val = 1234567, il sera affiché comme 

  No fractions: 1,234,567

Exemple et conseils supplémentaires sur: https://docs.angularjs.org/api/ng/filter/number

0
junktrunk