Mam tablicę Contact
obiektów:
var contacts:[Contact] = [Contact]()
Klasa kontaktu:
Class Contact:NSOBject {
var firstName:String!
var lastName:String!
}
I chciałbym, aby posortować według tej tablicy lastName
, a następnie firstName
w przypadku niektórych kontaktów dostał takie same lastName
.
Mogę sortować według jednego z tych kryteriów, ale nie obu.
contacts.sortInPlace({$0.lastName < $1.lastName})
Jak mogę dodać więcej kryteriów do sortowania tej tablicy?
Contact
prawdopodobnie nie powinien dziedziczyć poNSObject
, 2)Contact
prawdopodobnie powinien być strukturą i 3)firstName
ilastName
prawdopodobnie nie powinien być niejawnie rozpakowanymi opcjami.Odpowiedzi:
Pomyśl, co oznacza „sortowanie według wielu kryteriów”. Oznacza to, że dwa obiekty są najpierw porównywane według jednego kryterium. Następnie, jeśli te kryteria są takie same, remisy zostaną przełamane przez następne kryteria i tak dalej, aż uzyskasz żądaną kolejność.
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 } }
Widzisz tutaj
Sequence.sorted(by:)
metodę , która sprawdza podane zamknięcie, aby określić, jak elementy są porównywane.Jeśli twoje sortowanie będzie używane w wielu miejscach, może być lepiej, aby twój typ był zgodny z
Comparable
protokołem . W ten sposób możesz użyćSequence.sorted()
metody , która konsultuje twoją implementacjęComparable.<(_:_:)
operatora, aby określić, jak elementy są porównywane. W ten sposób można rozwiązać każdySequence
zContact
s bez konieczności duplikowania kodu sortowania.źródło
else
musi znajdować się między, w{ ... }
przeciwnym razie kod nie zostanie skompilowany.sort
vs.sortInPlace
zobacz tutaj . Aslo zobaczyć ten poniżej, jest dużo bardziej modularnasortInPlace
nie jest już dostępny w Swift 3, zamiast tego musisz go użyćsort()
.sort()
zmieni samą tablicę. Jest też nowa funkcja o nazwie,sorted()
która zwróci posortowaną tablicę==
nie jest dobrym pomysłem. Działa tylko dla 2 właściwości. Cokolwiek więcej, i zaczynasz powtarzać się z wieloma złożonymi wyrażeniami boolowskimiUżywanie krotek do porównywania wielu kryteriów
Naprawdę prostym sposobem sortowania według wielu kryteriów (tj. Sortowania według jednego porównania, a jeśli jest równoważne, to przez inne porównanie) jest użycie krotek , ponieważ operatory
<
i>
mają dla nich przeciążenia, które wykonują porównania leksykograficzne./// 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
Na przykład:
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") // ]
Spowoduje to najpierw porównanie
lastName
właściwości elementów . Jeśli nie są równe, kolejność sortowania będzie oparta na<
porównaniu z nimi. Jeśli są równe, to przejdzie do następnej pary elementów w krotce, czyli porównującfirstName
właściwości.Biblioteka standardowa zapewnia
<
i>
przeciąża krotki od 2 do 6 elementów.Jeśli chcesz mieć różne porządki sortowania dla różnych właściwości, możesz po prostu zamienić elementy w krotkach:
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"), // ]
To będzie teraz sortowane
lastName
malejąco, a następniefirstName
rosnąco.Definiowanie
sort(by:)
przeciążenia, które przyjmuje wiele predykatówZainspirowana dyskusją na temat sortowania kolekcji z
map
domknięciami i SortDescriptors , inną opcją byłoby zdefiniowanie niestandardowego przeciążeniasort(by:)
isorted(by:)
zajmującego się wieloma predykatami - gdzie każdy predykat jest z kolei brany pod uwagę przy decydowaniu o kolejności elementów.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 } } }
(
secondPredicate:
Parametr jest niefortunny, ale jest wymagany, aby uniknąć tworzenia niejednoznaczności z istniejącymsort(by:)
przeciążeniem)To pozwala nam powiedzieć (używając
contacts
wcześniejszej tablicy):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 // ... )
Chociaż witryna wywoławcza nie jest tak zwięzła jak wariant krotki, zyskujesz dodatkową jasność co do tego, co jest porównywane i w jakiej kolejności.
Zgodne z
Comparable
Jeśli zamierzasz regularnie przeprowadzać tego rodzaju porównania, to, jak sugerują @AMomchilov i @appzYourLife , możesz dostosować się
Contact
doComparable
: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) } }
A teraz po prostu poproś
sort()
o kolejność rosnącą:contacts.sort()
lub
sort(by: >)
malejąco:contacts.sort(by: >)
Definiowanie niestandardowych porządków sortowania w typie zagnieżdżonym
Jeśli masz inne porządki sortowania, których chcesz użyć, możesz zdefiniować je w typie zagnieżdżonym:
extension Contact { enum Comparison { static let firstLastAscending: (Contact, Contact) -> Bool = { return ($0.firstName, $0.lastName) < ($1.firstName, $1.lastName) } } }
a następnie zadzwoń jako:
contacts.sort(by: Contact.Comparison.firstLastAscending)
źródło
contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) }
Pomógł. Dziękicontacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }
.""
porównaniu z innymi ciągami (występuje przed niepustymi ciągami). Jest to rodzaj ukryty, magiczny i nieelastyczny, jeśli chcesz,nil
aby zamiast tego znajdowały się na końcu listy. Polecam przyjrzeć się mojejnilComparator
funkcji stackoverflow.com/a/44808567/3141234Poniżej przedstawiono inne proste podejście do sortowania według 2 kryteriów.
Sprawdź pierwsze pole, w tym przypadku tak jest
lastName
, jeśli nie są równe, posortuj wedługlastName
, jeślilastName
są równe, a następnie posortuj według drugiego pola, w tym przypadkufirstName
.contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName }
źródło
Jedyną rzeczą, której sortowania leksykograficzne nie mogą zrobić zgodnie z opisem @Hamish, jest obsługa różnych kierunków sortowania, powiedzmy sortowanie według pierwszego pola malejąco, następne pole rosnąco itp.
Stworzyłem post na blogu o tym, jak to zrobić w Swift 3 i utrzymywać kod prosty i czytelny.
Znajdziesz go tutaj:
http://master-method.com/index.php/2016/11/23/sort-a-sequence-ie-arrays-of-objects-by-multiple-properties-in-swift-3/Możesz również znaleźć repozytorium GitHub z kodem tutaj:
https://github.com/jallauca/SortByMultipleFieldsSwift.playground
Podsumowując, powiedzmy, że jeśli masz listę lokalizacji, będziesz w stanie to zrobić:
struct Location { var city: String var county: String var state: String } var locations: [Location] { return [ Location(city: "Dania Beach", county: "Broward", state: "Florida"), Location(city: "Fort Lauderdale", county: "Broward", state: "Florida"), Location(city: "Hallandale Beach", county: "Broward", state: "Florida"), Location(city: "Delray Beach", county: "Palm Beach", state: "Florida"), Location(city: "West Palm Beach", county: "Palm Beach", state: "Florida"), Location(city: "Savannah", county: "Chatham", state: "Georgia"), Location(city: "Richmond Hill", county: "Bryan", state: "Georgia"), Location(city: "St. Marys", county: "Camden", state: "Georgia"), Location(city: "Kingsland", county: "Camden", state: "Georgia"), ] } let sortedLocations = locations .sorted(by: ComparisonResult.flip <<< Location.stateCompare, Location.countyCompare, Location.cityCompare )
źródło
To pytanie ma już wiele świetnych odpowiedzi, ale chcę wskazać artykuł - Sort Descriptors in Swift . Istnieje kilka sposobów sortowania według wielu kryteriów.
Używając NSSortDescriptor, ten sposób ma pewne ograniczenia, obiekt powinien być klasą i dziedziczy po 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), ]
Tutaj, na przykład, chcemy posortować według nazwiska, następnie imienia, a na końcu roku urodzenia. Chcemy to robić bez rozróżniania wielkości liter i używając ustawień regionalnych użytkownika.
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)]
Korzystanie z szybkiego sposobu sortowania według nazwiska / imienia. Ten sposób powinien działać zarówno z klasą / strukturą. Jednak nie sortujemy tutaj według roku urodzenia.
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)]
Szybki sposób na zainicjowanie NSSortDescriptor. Wykorzystuje to koncepcję, że „funkcje są pierwszorzędnym typem”. SortDescriptor jest typem funkcji, przyjmuje dwie wartości, zwraca wartość bool. Powiedz sortByFirstName, bierzemy dwa parametry ($ 0, $ 1) i porównujemy ich imiona. Funkcja łączenia pobiera kilka SortDescriptors, porównuje je wszystkie i wydaje rozkazy.
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)]
Jest to dobre, ponieważ możesz go używać zarówno z klasą, jak i strukturą, możesz nawet rozszerzyć, aby porównać z nils.
Mimo to zdecydowanie zalecamy przeczytanie oryginalnego artykułu . Ma znacznie więcej szczegółów i jest dobrze wyjaśniony.
źródło
Polecam użycie rozwiązania krotki Hamisha, ponieważ nie wymaga to dodatkowego kodu.
Jeśli chcesz czegoś, co zachowuje się jak
if
instrukcje, ale upraszcza logikę rozgałęziania, możesz użyć tego rozwiązania, które pozwala wykonać następujące czynności:animals.sort { return comparisons( compare($0.family, $1.family, ascending: false), compare($0.name, $1.name)) }
Oto funkcje, które pozwalają to zrobić:
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 }
Jeśli chcesz to przetestować, możesz użyć tego dodatkowego kodu:
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), ]
Główną różnicą w stosunku do rozwiązania Jamiego jest to, że dostęp do właściwości jest definiowany w klasie, a nie jako metody statyczne / instancyjne. Na przykład
$0.family
ZamiastAnimal.familyCompare
. Rosnąco / malejąco jest kontrolowany przez parametr, a nie przez przeciążony operator. Rozwiązanie Jamiego dodaje rozszerzenie do Array, podczas gdy moje rozwiązanie korzysta z wbudowanej metodysort
/sorted
, ale wymaga zdefiniowania dwóch dodatkowych:compare
icomparisons
.Ze względu na kompletność, oto porównanie mojego rozwiązania z rozwiązaniem krotki Hamisha . Aby zademonstrować, użyję dzikiego przykładu, w którym chcemy posortować ludzi według
(name, address, profileViews)
rozwiązania Hamisha, oceni każdą z 6 wartości właściwości dokładnie raz przed rozpoczęciem porównania. Może to nie być pożądane lub nie. Na przykład zakładając, żeprofileViews
jest to drogie połączenie sieciowe, możemy chcieć uniknąć dzwonienia,profileViews
chyba że jest to absolutnie konieczne. Moje rozwiązanie pozwoli uniknąć ocenyprofileViews
do$0.name == $1.name
i$0.address == $1.address
. Jednak kiedy to oceniprofileViews
, prawdopodobnie oceni wielokrotnie więcej niż raz.źródło
Co powiesz na:
contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
źródło
lexicographicallyPrecedes
wymaga, aby wszystkie typy w tablicy były takie same. Na przykład[String, String]
. To, czego prawdopodobnie chce OP, to mieszanie i dopasowywanie typów:[String, Int, Bool]
aby mogli to zrobić[$0.first, $0.age, $0.isActive]
.który działał dla mojej tablicy [String] w Swift 3 i wydaje się, że w Swift 4 jest w porządku
array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}
źródło