Swift - Sortuj tablicę obiektów według wielu kryteriów

92

Mam tablicę Contactobiektó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 firstNamew 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?

sbkl
źródło
2
Zrób to dokładnie tak samo, jak powiedziałeś! Twój kod w nawiasach klamrowych powinien brzmieć: „Jeśli nazwiska są takie same, sortuj według imienia; w przeciwnym razie sortuj według nazwiska”.
mat.
4
Widzę tutaj kilka zapachów kodu: 1) Contactprawdopodobnie nie powinien dziedziczyć po NSObject, 2) Contactprawdopodobnie powinien być strukturą i 3) firstNamei lastNameprawdopodobnie nie powinien być niejawnie rozpakowanymi opcjami.
Alexander - Przywróć Monikę
3
@AMomchilov Nie ma powodu, by sugerować, że Contact powinien być strukturą, ponieważ nie wiesz, czy reszta jego kodu opiera się już na semantyce odwołań przy używaniu jej instancji.
Patrick Goley,
3
@AMomchilov „Prawdopodobnie” jest mylące, ponieważ nie wiesz dokładnie nic o pozostałej części kodu. Jeśli zostanie zmieniony na strukturę, wszystkie nagłe kopie są generowane podczas mutowania vars, zamiast modyfikowania instancji. Jest to drastyczna zmiana w zachowaniu, która „prawdopodobnie” spowodowałaby błędy, ponieważ jest mało prawdopodobne, że wszystko zostało poprawnie zakodowane zarówno dla semantyki referencji, jak i wartości.
Patrick Goley
1
@AMomchilov Nie słyszałem jeszcze jednego powodu, dla którego prawdopodobnie powinna to być struktura. Myślę, że OP nie byłby w stanie docenić sugestii modyfikujących semantykę reszty jego programu, zwłaszcza gdy nie było nawet konieczne rozwiązywanie problemu. Nie zdawałem sobie sprawy, że dla niektórych reguły kompilatora są prawnicze ... może jestem na złej stronie
Patrick Goley

Odpowiedzi:

120

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żdy Sequencez Contacts bez konieczności duplikowania kodu sortowania.

Alexander - Przywróć Monikę
źródło
2
Treść elsemusi znajdować się między, w { ... }przeciwnym razie kod nie zostanie skompilowany.
Luca Angeletti
Rozumiem. Próbowałem go zaimplementować, ale nie mogłem uzyskać prawidłowej składni. Wielkie dzięki.
sbkl
dla sortvs. sortInPlacezobacz tutaj . Aslo zobaczyć ten poniżej, jest dużo bardziej modularna
Miód
sortInPlacenie 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ę
Honey
2
@AthanasiusOfAlex Używanie ==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 boolowskimi
Alexander - Przywróć Monikę
123

Uż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 lastNamewłaściwości elementów . Jeśli nie są równe, kolejność sortowania będzie oparta na <porównaniu z nimi. Jeśli równe, to przejdzie do następnej pary elementów w krotce, czyli porównując firstNamewł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 lastNamemalejąco, a następnie firstNamerosnąco.


Definiowanie sort(by:)przeciążenia, które przyjmuje wiele predykatów

Zainspirowana dyskusją na temat sortowania kolekcji z mapdomknięciami i SortDescriptors , inną opcją byłoby zdefiniowanie niestandardowego przeciążenia sort(by:)i sorted(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ącym sort(by:)przeciążeniem)

To pozwala nam powiedzieć (używając contactswcześ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ę Contactdo 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)
  }
}

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)
Hamish
źródło
contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) } Pomógł. Dzięki
Prabhakar Kasi
Jeśli tak jak ja, właściwości mają być posortowane są OPCJE, a następnie można zrobić coś takiego: contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }.
BobCowe
Holly Molly! Tak proste, ale tak wydajne ... dlaczego nigdy o tym nie słyszałem ?! Wielkie dzięki!
Etenyl
@BobCowe To pozostawia Cię na łasce tego, jak wypada w ""porównaniu z innymi ciągami (występuje przed niepustymi ciągami). Jest to rodzaj ukryty, magiczny i nieelastyczny, jeśli chcesz, nilaby zamiast tego znajdowały się na końcu listy. Polecam przyjrzeć się mojej nilComparatorfunkcji stackoverflow.com/a/44808567/3141234
Alexander - Przywróć Monikę
19

Poniż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ług lastName, jeśli lastNamesą równe, a następnie posortuj według drugiego pola, w tym przypadku firstName.

contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName  }
oyalhi
źródło
Daje to większą elastyczność niż krotki.
Babac
5

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
        )
Jaime Allauca
źródło
1
„Jedyną rzeczą, której sortowania leksykograficzne nie mogą zrobić zgodnie z opisem @Hamish, jest obsługa różnych kierunków sortowania” - tak, mogą, po prostu zamień elementy w krotki;)
Hamish
Uważam, że jest to interesujące ćwiczenie teoretyczne, ale znacznie bardziej skomplikowane niż odpowiedź @ Hamisha. Moim zdaniem mniej kodu to lepszy kod.
Manuel
5

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.

  1. 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)]
    
  2. 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)]
    
  3. 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.

XueYu
źródło
2

Polecam użycie rozwiązania krotki Hamisha, ponieważ nie wymaga to dodatkowego kodu.


Jeśli chcesz czegoś, co zachowuje się jak ifinstrukcje, 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 Zamiast Animal.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 metody sort/ sorted, ale wymaga zdefiniowania dwóch dodatkowych: comparei comparisons.

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, że profileViewsjest to drogie połączenie sieciowe, możemy chcieć uniknąć dzwonienia, profileViewschyba że jest to absolutnie konieczne. Moje rozwiązanie pozwoli uniknąć oceny profileViewsdo $0.name == $1.namei $0.address == $1.address. Jednak kiedy to oceni profileViews, prawdopodobnie oceni wielokrotnie więcej niż raz.

Rozsądne
źródło
1

Co powiesz na:

contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
Lou Zell
źródło
lexicographicallyPrecedeswymaga, 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].
Senseful
-1

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}
mamaz
źródło
Czy przeczytałeś pytanie przed udzieleniem odpowiedzi? Sortuj według wielu parametrów, a nie jednego, co prezentujesz.
Vive