Usando tuplas para fazer uma comparação de vários critérios
Uma maneira realmente simples de realizar uma classificação por vários critérios (ou seja, classificar por uma comparação e, se equivalente, por outra comparação) é usando tuplas , já que os operadores <
e >
têm sobrecargas para eles que realizam comparações lexicográficas.
/// Returns a Boolean value indicating whether the first tuple is ordered
/// before the second in a lexicographical ordering.
///
/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is before the second tuple if and only if
/// `a1 < b1` or (`a1 == b1` and
/// `(a2, ..., aN) < (b2, ..., bN)`).
public func < <A : Comparable, B : Comparable>(lhs: (A, B), rhs: (A, B)) -> Bool
Por exemplo:
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")
// ]
Isso irá comparar as lastName
propriedades dos elementos primeiro. Se eles não forem iguais, a ordem de classificação será baseada em uma <
comparação com eles. Se eles forem iguais, ele se moverá para o próximo par de elementos na tupla, ou seja, comparando as firstName
propriedades.
A biblioteca padrão fornece <
e >
sobrecarrega tuplas de 2 a 6 elementos.
Se você quiser diferentes ordens de classificação para diferentes propriedades, pode simplesmente trocar os elementos nas tuplas:
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"),
// ]
Agora, isso será classificado por ordem lastName
decrescente e depois firstName
crescente.
Definindo uma sort(by:)
sobrecarga que leva vários predicados
Inspirado na discussão sobre Sorting Collections with map
closures e SortDescriptors , outra opção seria definir uma sobrecarga customizada de sort(by:)
e sorted(by:)
que lida com vários predicados - onde cada predicado é considerado por sua vez para decidir a ordem dos elementos.
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 {
mutating 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, rhs) { return true }
if predicate(rhs, lhs) { return false }
}
return false
}
}
}
(O secondPredicate:
parâmetro é lamentável, mas é necessário para evitar a criação de ambigüidades com a sort(by:)
sobrecarga existente )
Isso então nos permite dizer (usando a contacts
matriz anterior):
contacts.sort(by:
{ $0.lastName > $1.lastName }, // first sort by lastName descending
{ $0.firstName < $1.firstName } // ... then firstName ascending
// ...
)
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"),
// ]
// or with sorted(by:)...
let sortedContacts = contacts.sorted(by:
{ $0.lastName > $1.lastName }, // first sort by lastName descending
{ $0.firstName < $1.firstName } // ... then firstName ascending
// ...
)
Embora o call-site não seja tão conciso quanto a variante de tupla, você ganha clareza adicional com o que está sendo comparado e em que ordem.
De acordo com Comparable
Se você vai fazer esse tipo de comparação regularmente, como @AMomchilov e @appzYourLife sugerem, você pode se conformar Contact
a 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)
}
}
E agora é só pedir sort()
uma ordem crescente:
contacts.sort()
ou sort(by: >)
para uma ordem decrescente:
contacts.sort(by: >)
Definição de ordens de classificação personalizadas em um tipo aninhado
Se você tiver outras ordens de classificação que deseja usar, poderá defini-las em um tipo aninhado:
extension Contact {
enum Comparison {
static let firstLastAscending: (Contact, Contact) -> Bool = {
return ($0.firstName, $0.lastName) <
($1.firstName, $1.lastName)
}
}
}
e simplesmente chame como:
contacts.sort(by: Contact.Comparison.firstLastAscending)