J'ai un tableau d'objets Contact
:
var contacts:[Contact] = [Contact]()
Classe de contact:
Class Contact:NSOBject {
var firstName:String!
var lastName:String!
}
Et je voudrais trier ce tableau par lastName
et ensuite par firstName
au cas où certains contacts auraient le même lastName
.
Je suis capable de trier selon l'un de ces critères, mais pas les deux.
contacts.sortInPlace({$0.lastName < $1.lastName})
Comment pourrais-je ajouter plus de critères pour trier ce tableau?
Pensez à ce que signifie "trier selon plusieurs critères". Cela signifie que deux objets sont d'abord comparés par un critère. Ensuite, si ces critères sont identiques, les égalités seront rompues selon les critères suivants, et ainsi de suite jusqu'à ce que vous obteniez l'ordre souhaité.
let sortedContacts = contacts.sort {
if $0.lastName != $1.lastName { // first, compare by last names
return $0.lastName < $1.lastName
}
/* last names are the same, break ties by foo
else if $0.foo != $1.foo {
return $0.foo < $1.foo
}
... repeat for all other fields in the sorting
*/
else { // All other fields are tied, break ties by last name
return $0.firstName < $1.firstName
}
}
Ce que vous voyez ici est la méthode Sequence.sorted(by:)
, qui consulte la fermeture fournie pour déterminer comment les éléments se comparent.
Si votre tri doit être utilisé à de nombreux endroits, il peut être préférable de rendre votre type conforme au protocole Comparable
. De cette façon, vous pouvez utiliser Sequence.sorted()
, méthode , qui consulte votre implémentation de l'opérateur Comparable.<(_:_:)
pour déterminer la comparaison des éléments. De cette façon, vous pouvez trier n'importe quelle Sequence
de Contact
s sans avoir à dupliquer le code de tri.
Un moyen très simple d'effectuer un tri selon plusieurs critères (c'est-à-dire le tri par comparaison et, le cas échéant, par comparaison) consiste à utiliser tuples, car les opérateurs <
et >
disposent de surcharges permettant d'effectuer des comparaisons lexicographiques. .
/// 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
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 les propriétés lastName
des éléments en premier. S'ils ne sont pas égaux, l'ordre de tri sera basé sur une comparaison de <
avec eux. S'ils sont égaux, il passera à la paire d'éléments suivante dans le nuplet, c'est-à-dire en comparant les propriétés firstName
.
La bibliothèque standard fournit des surcharges <
et >
pour des nuplets de 2 à 6 éléments.
Si vous voulez différents ordres de tri pour différentes propriétés, vous pouvez simplement échanger les éléments dans les n-uplets:
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 va maintenant trier par lastName
décroissant, puis firstName
croissant.
sort(by:)
prenant plusieurs prédicatsInspiré par la discussion sur Tri des collections avec map
fermetures et SortDescriptors , une autre option serait de définir une surcharge personnalisée de sort(by:)
et sorted(by:)
qui traite de plusieurs prédicats - chaque prédicat étant pris en compte 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 {
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
}
}
}
(Le paramètre secondPredicate:
est regrettable, mais il est nécessaire pour éviter de créer des ambiguïtés avec la surcharge sort(by:)
existante.)
Cela nous permet ensuite de dire (en utilisant le tableau contacts
de plus tôt):
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
// ...
)
Bien que le site d’appel ne soit pas aussi concis que la variante Tuple, vous obtenez une clarté supplémentaire avec ce qui est comparé et dans quel ordre.
Comparable
Si vous comptez effectuer ce type de comparaison régulièrement, comme le suggère @AMomchilov & @appzYourLife , vous pouvez vous conformer Contact
à 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, appelez simplement sort()
pour un ordre croissant:
contacts.sort()
ou sort(by: >)
pour un ordre décroissant:
contacts.sort(by: >)
Si vous souhaitez utiliser d'autres ordres de tri, 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)
Une autre approche simple pour trier avec 2 critères est présentée ci-dessous.
Recherchez le premier champ, dans ce cas, il s'agit de lastName
, s'ils ne sont pas égaux, triez par lastName
, si lastName
sont égaux, puis triez par le deuxième champ, dans ce cas firstName
.
contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName }
Cette question a déjà de nombreuses bonnes réponses, mais je voudrais citer un article - Sort Descriptors in Swift . Nous avons plusieurs façons de faire le tri selon plusieurs critères.
En utilisant NSSortDescriptor, cette manière a certaines limitations, l'objet doit être une classe et hérite de NSObject.
class Person: NSObject {
var first: String
var last: String
var yearOfBirth: Int
init(first: String, last: String, yearOfBirth: Int) {
self.first = first
self.last = last
self.yearOfBirth = yearOfBirth
}
override var description: String {
get {
return "\(self.last) \(self.first) (\(self.yearOfBirth))"
}
}
}
let people = [
Person(first: "Jo", last: "Smith", yearOfBirth: 1970),
Person(first: "Joe", last: "Smith", yearOfBirth: 1970),
Person(first: "Joe", last: "Smyth", yearOfBirth: 1970),
Person(first: "Joanne", last: "smith", yearOfBirth: 1985),
Person(first: "Joanne", last: "smith", yearOfBirth: 1970),
Person(first: "Robert", last: "Jones", yearOfBirth: 1970),
]
Ici, par exemple, nous voulons trier par nom de famille, puis prénom, enfin par année de naissance. Et nous voulons le faire sans distinction de casse et en utilisant les paramètres régionaux de l'utilisateur.
let lastDescriptor = NSSortDescriptor(key: "last", ascending: true,
selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
let firstDescriptor = NSSortDescriptor(key: "first", ascending: true,
selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
let yearDescriptor = NSSortDescriptor(key: "yearOfBirth", ascending: true)
(people as NSArray).sortedArray(using: [lastDescriptor, firstDescriptor, yearDescriptor])
// [Robert Jones (1970), Jo Smith (1970), Joanne smith (1970), Joanne smith (1985), Joe Smith (1970), Joe Smyth (1970)]
Utilisation de la méthode de tri rapide avec nom de famille/prénom ..__ Cette méthode devrait fonctionner avec les deux classes/struct. Cependant, nous ne faisons pas de tri par année de naissance ici.
let sortedPeople = people.sorted { p0, p1 in
let left = [p0.last, p0.first]
let right = [p1.last, p1.first]
return left.lexicographicallyPrecedes(right) {
$0.localizedCaseInsensitiveCompare($1) == .orderedAscending
}
}
sortedPeople // [Robert Jones (1970), Jo Smith (1970), Joanne smith (1985), Joanne smith (1970), Joe Smith (1970), Joe Smyth (1970)]
Manière rapide d'introduire NSSortDescriptor. Ceci utilise le concept que "les fonctions sont un type de première classe". SortDescriptor est un type de fonction, prend deux valeurs, retourne un bool. Disons sortByFirstName nous prenons deux paramètres ($ 0, $ 1) et comparons leurs prénoms. Les fonctions de combinaison prennent un tas de SortDescriptors, les comparent tous et donnent des ordres.
typealias SortDescriptor<Value> = (Value, Value) -> Bool
let sortByFirstName: SortDescriptor<Person> = {
$0.first.localizedCaseInsensitiveCompare($1.first) == .orderedAscending
}
let sortByYear: SortDescriptor<Person> = { $0.yearOfBirth < $1.yearOfBirth }
let sortByLastName: SortDescriptor<Person> = {
$0.last.localizedCaseInsensitiveCompare($1.last) == .orderedAscending
}
func combine<Value>
(sortDescriptors: [SortDescriptor<Value>]) -> SortDescriptor<Value> {
return { lhs, rhs in
for isOrderedBefore in sortDescriptors {
if isOrderedBefore(lhs,rhs) { return true }
if isOrderedBefore(rhs,lhs) { return false }
}
return false
}
}
let combined: SortDescriptor<Person> = combine(
sortDescriptors: [sortByLastName,sortByFirstName,sortByYear]
)
people.sorted(by: combined)
// [Robert Jones (1970), Jo Smith (1970), Joanne smith (1970), Joanne smith (1985), Joe Smith (1970), Joe Smyth (1970)]
C'est bien parce que vous pouvez l'utiliser à la fois avec struct et class, vous pouvez même l'étendre pour le comparer avec nils.
Néanmoins, la lecture de l’article original est fortement recommandée. Il a beaucoup plus de détails et bien expliqué.
Je recommanderais d'utiliser la solution Tuple de Hamish car elle ne nécessite pas de code supplémentaire.
Si vous voulez quelque chose qui se comporte comme les instructions if
mais simplifie la logique de branchement, vous pouvez utiliser cette solution, ce qui vous permet d'effectuer les opérations suivantes:
animals.sort {
return comparisons(
compare($0.family, $1.family, ascending: false),
compare($0.name, $1.name))
}
Voici les fonctions qui vous permettent de faire ceci:
func compare<C: Comparable>(_ value1Closure: @autoclosure @escaping () -> C, _ value2Closure: @autoclosure @escaping () -> C, ascending: Bool = true) -> () -> ComparisonResult {
return {
let value1 = value1Closure()
let value2 = value2Closure()
if value1 == value2 {
return .orderedSame
} else if ascending {
return value1 < value2 ? .orderedAscending : .orderedDescending
} else {
return value1 > value2 ? .orderedAscending : .orderedDescending
}
}
}
func comparisons(_ comparisons: (() -> ComparisonResult)...) -> Bool {
for comparison in comparisons {
switch comparison() {
case .orderedSame:
continue // go on to the next property
case .orderedAscending:
return true
case .orderedDescending:
return false
}
}
return false // all of them were equal
}
Si vous voulez le tester, vous pouvez utiliser ce code supplémentaire:
enum Family: Int, Comparable {
case bird
case cat
case dog
var short: String {
switch self {
case .bird: return "B"
case .cat: return "C"
case .dog: return "D"
}
}
public static func <(lhs: Family, rhs: Family) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
struct Animal: CustomDebugStringConvertible {
let name: String
let family: Family
public var debugDescription: String {
return "\(name) (\(family.short))"
}
}
let animals = [
Animal(name: "Leopard", family: .cat),
Animal(name: "Wolf", family: .dog),
Animal(name: "Tiger", family: .cat),
Animal(name: "Eagle", family: .bird),
Animal(name: "Cheetah", family: .cat),
Animal(name: "Hawk", family: .bird),
Animal(name: "Puma", family: .cat),
Animal(name: "Dalmatian", family: .dog),
Animal(name: "Lion", family: .cat),
]
La principale différence par rapport à la solution de Jamie est que l'accès aux propriétés est défini en ligne plutôt qu'en tant que méthode statique/instance sur la classe. Par exemple. $0.family
au lieu de Animal.familyCompare
. Et ascendant/descendant est contrôlé par un paramètre plutôt que par un opérateur surchargé. La solution de Jamie ajoute une extension à Array alors que ma solution utilise la méthode sort
/sorted
intégrée, mais nécessite la définition de deux méthodes supplémentaires: compare
et comparisons
.
Par souci d'exhaustivité, voici comment ma solution se compare à la solution de Hamish's Tuple . Pour illustrer mon propos, je vais utiliser un exemple sauvage dans lequel nous voulons trier les personnes par (name, address, profileViews)
La solution de Hamish évaluera chacune des 6 valeurs de propriété exactement une fois avant le début de la comparaison. Cela peut ne pas être ou ne pas être désiré. Par exemple, en supposant que profileViews
est un appel réseau coûteux, nous voudrons peut-être éviter d'appeler profileViews
à moins que ce ne soit absolument nécessaire. Ma solution évitera d’évaluer profileViews
jusqu’à $0.name == $1.name
et $0.address == $1.address
. Cependant, quand il évaluera profileViews
, il en évaluera probablement beaucoup plus de fois.
Que diriez-vous:
contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
cela a fonctionné pour mon tableau [String] dans Swift 3 et il semble dans Swift 4 est ok
array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}