Underscore: sortBy () basé sur plusieurs attributs

115

J'essaye de trier un tableau avec des objets basés sur plusieurs attributs. C'est-à-dire que 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érez 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 par l' roomNumberattribut, j'utiliserais le code suivant:

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

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

Christian R
la source

Réponses:

250

sortBy dit qu'il s'agit d'un algorithme de tri stable, vous devriez donc être en mesure de trier d'abord par votre deuxième propriété, puis de trier à nouveau par 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 le second sortBydécouvre que John et Lisa ont le même numéro de chambre, il les conservera dans l'ordre dans lequel il les a trouvés, ce que le premier a sortBydéfini sur "Lisa, John".

Rory MacLeod
la source
12
Il existe un article de blog qui développe cela et comprend de bonnes informations sur le tri des propriétés ascendantes et descendantes.
Alex C
4
Une solution plus simple au tri chaîné peut être trouvée ici . Pour être honnête, il semble que l'article de blog ait été écrit après que ces réponses aient été données, mais cela m'a aidé à comprendre cela après avoir essayé d'utiliser le code dans la réponse ci-dessus et échoué.
Mike Devenney
1
Vous êtes sûr que le patient [0] .name et le patient [1] .roomNumber devraient avoir l'index? patient n'est pas un tableau ...
StinkyCat
L' [0]indexeur est requis car dans l'exemple d'origine, patientsest un tableau de tableaux. C'est aussi pourquoi la «solution plus simple» dans le billet de blog mentionné dans un autre commentaire ne fonctionnera pas ici.
Rory MacLeod
1
@ac_fire Voici une archive de ce lien maintenant mort: archive.is/tiatQ
lustig
52

Voici une astuce hacky que j'utilise parfois dans ces cas: combinez les propriétés de manière à ce que le résultat soit triable:

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

Cependant, comme je l'ai dit, c'est assez hacky. Pour faire cela correctement, vous voudrez probablement utiliser la sortméthode 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;
}

Bien sûr, cela triera votre tableau en place. Si vous voulez une copie triée (comme _.sortByvous le donnez), 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 (pour trier par un nombre arbitraire de clés) pour cela aussi: jetez un œil .

Dan Tao
la source
Merci beaucoup pour cette solution qui a fini par utiliser la seconde car mes attributs peuvent être à la fois des chaînes et des nombres. Il ne semble donc pas y avoir de moyen natif simple de trier les tableaux?
Christian R
3
Pourquoi ne suffit-il return [patient[0].roomNumber, patient[0].name];pas sans le join?
Csaba Toth
1
Le lien vers votre solution générale semble être rompu (ou peut-être que je ne parviens pas à y accéder via notre serveur proxy). Pourriez-vous s'il vous plaît l'afficher ici?
Zev Spitz
En outre, comment les comparevaleurs de poignée qui ne sont pas des valeurs primitives - undefined, nullou des objets simples?
Zev Spitz
Pour info, ce hack ne fonctionne que si vous vous assurez que la longueur de chaîne de chaque valeur est la même pour tous les éléments du tableau.
miex
32

Je sais que je suis en retard à la fête, 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 par ordre de propriété la moins importante à la propriété la plus importante. Dans le code ci-dessous, je crée un nouveau tableau de patients triés par nom dans RoomNumber à partir du tableau d'origine appelé patients .

var sortedPatients = _.chain(patients)
  .sortBy('Name')
  .sortBy('RoomNumber')
  .value();
Mike Devenney
la source
4
Même si vous êtes en retard, vous avez toujours raison :) Merci!
Allan Jikamu
3
Nice, très propre.
Jason Turan
11

btw votre initialiseur pour les patients est un peu bizarre, n'est-ce pas? pourquoi ne pas initialiser cette variable comme ceci - comme un vrai tableau d'objets - vous pouvez le faire en utilisant _.flatten () et non comme un tableau de tableaux d'un seul objet, c'est peut-être un problème de 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}
        ];

J'ai trié la liste différemment et ajouté 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 ceci

[
{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: utilisez un tableau dans votre fonction de rappel, c'est assez similaire à la réponse de Dan Tao , j'oublie juste la jointure (peut-être parce que j'ai supprimé le tableau de tableaux de l'élément unique :))
En utilisant votre structure de données, alors il serait :

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

et un testload serait intéressant ...

zobidafly
la source
Sérieusement, c'est la réponse
Radek Duchoň
7

Aucune de ces réponses n'est idéale comme méthode à usage général pour utiliser plusieurs champs dans un tri. Toutes les approches ci-dessus sont inefficaces car elles nécessitent soit de trier le tableau plusieurs fois (ce qui, sur une liste suffisamment grande, pourrait ralentir beaucoup les choses), soit elles génèrent d'énormes quantités d'objets de déchets que la machine virtuelle devra nettoyer (et finalement ralentir le programme vers le bas).

Voici une solution rapide, efficace, permettant facilement le tri inversé et pouvant être utilisée avec underscoreoulodash , ou directement avecArray.sort

La partie la plus importante est la compositeComparatorméthode, qui prend un tableau de fonctions de comparaison et renvoie 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 sur lesquels vous souhaitez trier. La naturalSortfonction créera un comparateur à partir d'un champ particulier. Ecrire 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 triera par numéro de chambre, suivi du nom. L'ajout de critères de tri supplémentaires est simple 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 ce qui suit

[ { 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 ou n'effectue pas de concaténation de chaînes à l'intérieur du tri et peut facilement être utilisée pour que certaines colonnes soient triées à l'envers tandis que les colonnes d'ordre utilisent naturel Trier.

Andrew Newdigate
la source
2

Peut-être que le fichier underscore.js ou simplement les moteurs Javascript sont différents maintenant que lorsque ces réponses ont été écrites, 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/

Mike K
la source
2

Renvoyez simplement un tableau de propriétés avec lesquelles vous souhaitez trier:

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 aucun effet secondaire de la conversion d'un nombre en chaîne.

Chanceux Soni
la source
1

Vous pouvez concaténer les propriétés par lesquelles 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 devrez faire quelque chose si vous aviez des numéros de pièce> 10. Sinon, 11 viendra avant 2. Vous pouvez compléter avec des zéros non significatifs pour résoudre le problème, c'est-à-dire 01 au lieu de 1.

Mark Sherretta
la source
1

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

_.orderBy(patients, ['name', 'roomNumber'], ['asc', 'desc'])
ZhangYi
la source
4
Êtes-vous sûr que orderBy est en trait de soulignement? Je ne peux pas le voir dans la documentation ou dans mon fichier .d.ts.
Zachary Dow
1
Il n'y a pas de orderBy dans le trait de soulignement.
AfroMogli
1
_.orderByfonctionne, mais c'est une méthode de la bibliothèque lodash, et non un tiret bas: lodash.com/docs/4.17.4#orderBy lodash est principalement un remplacement instantané du trait de soulignement, donc il pourrait être approprié pour l'OP.
Mike K
0

Si vous utilisez Angular, vous pouvez utiliser son filtre numérique 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

junktrunk
la source