Utilisation des tuples pour comparer plusieurs critères
Une façon vraiment simple d'effectuer un tri selon plusieurs critères (c'est-à-dire trier selon une comparaison, et si équivalents, alors selon une autre comparaison) est d'utiliser des tuples, car les opérateurs <
et >
ont des surcharges pour eux qui effectuent des comparaisons lexicographiques.
/// Renvoie une valeur booléenne indiquant si le premier tuple est ordonné
/// avant le deuxième dans un ordre lexicographique.
///
/// Étant donné deux tuples `(a1, a2, ..., aN)` et `(b1, b2, ..., bN)`, le premier
/// tuple est avant le deuxième tuple si et seulement si
/// `a1 < b1` ou (`a1 == b1` et
/// `(a2, ..., aN) < (b2, ..., bN)`).
public func < (lhs: (A, B), rhs: (A, B)) -> Bool
Par exemple:
struct Contact {
var firstName: String
var lastName: String
}
var contacts = [
Contact(firstName: "Leonard", lastName: "Charleson"),
Contact(firstName: "Michael", lastName: "Webb"),
Contact(firstName: "Charles", lastName: "Alexson"),
Contact(firstName: "Michael", lastName: "Elexson"),
Contact(firstName: "Alex", lastName: "Elexson"),
]
contacts.sort {
($0.lastName, $0.firstName) <
($1.lastName, $1.firstName)
}
print(contacts)
// [
// Contact(firstName: "Charles", lastName: "Alexson"),
// Contact(firstName: "Leonard", lastName: "Charleson"),
// Contact(firstName: "Alex", lastName: "Elexson"),
// Contact(firstName: "Michael", lastName: "Elexson"),
// Contact(firstName: "Michael", lastName: "Webb")
// ]
Cela comparera d'abord les propriétés lastName
des éléments. S'ils ne sont pas égaux, alors l'ordre de tri sera basé sur une comparaison avec <
entre eux. S'ils sont égaux, alors cela passera au prochain couple d'éléments dans le tuple, c'est-à-dire en comparant les propriétés firstName
.
La bibliothèque standard fournit des surcharges <
et >
pour les tuples de 2 à 6 éléments.
Si vous voulez des ordres de tri différents pour différentes propriétés, vous pouvez simplement échanger les éléments dans les tuples:
contacts.sort {
($1.lastName, $0.firstName) <
($0.lastName, $1.firstName)
}
// [
// Contact(firstName: "Michael", lastName: "Webb")
// Contact(firstName: "Alex", lastName: "Elexson"),
// Contact(firstName: "Michael", lastName: "Elexson"),
// Contact(firstName: "Leonard", lastName: "Charleson"),
// Contact(firstName: "Charles", lastName: "Alexson"),
// ]
Cela triera maintenant par lastName
descendant, puis par firstName
ascendant.
Définition d'une surcharge de sort(by:)
qui prend plusieurs prédicats
En s'inspirant de la discussion sur Trier les collections avec des map
closures et des SortDescriptors, une autre option serait de définir une surcharge personnalisée de sort(by:)
et sorted(by:)
qui traite de multiples prédicats - où chaque prédicat est considéré tour à tour pour décider de l'ordre des éléments.
extension MutableCollection where Self : RandomAccessCollection {
mutating func sort(
by firstPredicate: (Element, Element) -> Bool,
_ secondPredicate: (Element, Element) -> Bool,
_ otherPredicates: ((Element, Element) -> Bool)...
) {
sort(by:) { lhs, rhs in
if firstPredicate(lhs, rhs) { return true }
if firstPredicate(rhs, lhs) { return false }
if secondPredicate(lhs, rhs) { return true }
if secondPredicate(rhs, lhs) { return false }
for predicate in otherPredicates {
if predicate(lhs, rhs) { return true }
if predicate(rhs, lhs) { return false }
}
return false
}
}
}
extension Sequence {
func sorted(
by firstPredicate: (Element, Element) -> Bool,
_ secondPredicate: (Element, Element) -> Bool,
_ otherPredicates: ((Element, Element) -> Bool)...
) -> [Element] {
return sorted(by:) { lhs, rhs in
if firstPredicate(lhs, rhs) { return true }
if firstPredicate(rhs, lhs) { return false }
if secondPredicate(lhs, rhs) { return true }
if secondPredicate(rhs, lhs) { return false }
for predicate in otherPredicates {
if predicate(lhs, ...
(Le paramètre secondPredicate:
est malheureux, mais il est nécessaire pour éviter de créer des ambiguïtés avec la surcharge existante de sort(by:)
)
Ensuite, nous pouvons dire (en utilisant le tableau contacts
précédent):
contacts.sort(by:
{ $0.lastName > $1.lastName }, // d'abord trier par lastName descendant
{ $0.firstName < $1.firstName } // ... puis par firstName ascendant
// ...
)
print(contacts)
// [
// Contact(firstName: "Michael", lastName: "Webb")
// Contact(firstName: "Alex", lastName: "Elexson"),
// Contact(firstName: "Michael", lastName: "Elexson"),
// Contact(firstName: "Leonard", lastName: "Charleson"),
// Contact(firstName: "Charles", lastName: "Alexson"),
// ]
// ou avec sorted(by:)...
let sortedContacts = contacts.sorted(by:
{ $0.lastName > $1.lastName }, // d'abord trier par lastName descendant
{ $0.firstName < $1.firstName } // ... puis par firstName ascendant
// ...
)
Bien que le site d'appel ne soit pas aussi concis que la variante de tuple, vous gagnez en clarté supplémentaire sur ce qui est comparé et dans quel ordre.
Conformité à Comparable
Si vous prévoyez de faire ce genre de comparaisons régulièrement alors, comme le suggèrent @AMomchilov & @appzYourLife, vous pouvez faire en sorte que Contact
soit conforme à Comparable
:
extension Contact : Comparable {
static func == (lhs: Contact, rhs: Contact) -> Bool {
return (lhs.firstName, lhs.lastName) ==
(rhs.firstName, rhs.lastName)
}
static func < (lhs: Contact, rhs: Contact) -> Bool {
return (lhs.lastName, lhs.firstName) <
(rhs.lastName, rhs.firstName)
}
}
Et maintenant il suffit d'appeler sort()
pour un ordre ascendant:
contacts.sort()
ou sort(by: >)
pour un ordre descendant:
contacts.sort(by: >)
Définition d'ordres de tri personnalisés dans un type imbriqué
Si vous avez d'autres ordres de tri que vous souhaitez utiliser, vous pouvez les définir dans un type imbriqué:
extension Contact {
enum Comparison {
static let firstLastAscending: (Contact, Contact) -> Bool = {
return ($0.firstName, $0.lastName) <
($1.firstName, $1.lastName)
}
}
}
et ensuite simplement appeler comme:
contacts.sort(by: Contact.Comparison.firstLastAscending)
2 votes
Faites exactement comme vous venez de le dire! Votre code à l'intérieur des accolades doit dire: "Si les noms de famille sont identiques, alors triez par prénom; sinon, triez par nom de famille".
4 votes
Je vois quelques odeurs de code ici: 1)
Contact
ne devrait probablement pas hériter deNSObject
, 2)Contact
devrait probablement être une struct, et 3)firstName
etlastName
ne devraient probablement pas être des optionnels déballés implicitement.3 votes
@AMomchilov Il n'y a aucune raison de suggérer que Contact devrait être une structure car vous ne savez pas si le reste de son code repose déjà sur la sémantique de référence en utilisant des instances de celle-ci.
1 votes
@PatrickGoley "...probablement..."
3 votes
@AMomchilov "Probablement" est trompeur car vous ne savez absolument rien sur le reste de la base de code. Si cela est changé en une structure, tout à coup des copies sont générées lors de la mutation des variables, au lieu de modifier l'instance en cours. C'est un changement drastique de comportement et le faire pourrait probablement entraîner des bugs car il est peu probable que tout a été correctement codé pour les sémantiques de référence et de valeur.
1 votes
@AMomchilov Je n'ai pas encore entendu une bonne raison pour laquelle cela devrait probablement être une structure. Je ne pense pas que l'OP apprécierait les suggestions qui modifient la sémantique du reste de son programme, surtout quand ce n'était même pas nécessaire pour résoudre le problème en cours. Je n'avais pas réalisé que les règles du compilateur étaient du jargon pour certains... peut-être que je suis sur le mauvais site web.
0 votes
Laissez-nous continuer cette discussion en chat.