Swift: ordina array di oggetti con più criteri


91

Ho una serie di Contactoggetti:

var contacts:[Contact] = [Contact]()

Classe di contatto:

Class Contact:NSOBject {
    var firstName:String!
    var lastName:String!
}

E vorrei ordinare quell'array prima lastNamee poi per firstNamenel caso in cui alcuni contatti ottengano lo stesso lastName.

Sono in grado di ordinare in base a uno di questi criteri, ma non entrambi.

contacts.sortInPlace({$0.lastName < $1.lastName})

Come posso aggiungere più criteri per ordinare questo array?


2
Fallo esattamente come hai appena detto! Il tuo codice all'interno delle parentesi graffe dovrebbe dire: "Se i cognomi sono gli stessi, allora ordina per nome; altrimenti ordina per cognome".
matt

4
Vedo alcuni odori di codice qui: 1) Contactprobabilmente non dovrebbe ereditare da NSObject, 2) Contactprobabilmente dovrebbe essere una struttura e 3) firstNamee lastNameprobabilmente non dovrebbero essere optionals implicitamente scartate.
Alexander - Ripristina Monica il

3
@AMomchilov Non c'è motivo di suggerire che Contact dovrebbe essere una struttura perché non si sa se il resto del suo codice si basa già sulla semantica di riferimento nell'utilizzo di istanze di esso.
Patrick Goley

3
@AMomchilov "Probabilmente" è fuorviante perché non sai esattamente nulla del resto del codice. Se viene modificato in una struttura, tutte le copie improvvise vengono generate quando si mutano i var, invece di modificare l'istanza in questione. Si tratta di un drastico cambiamento nel comportamento e ciò comporterebbe "probabilmente" dei bug perché è improbabile che tutto sia stato codificato correttamente sia per la semantica di riferimento che per quella del valore.
Patrick Goley

1
@AMomchilov Devo ancora sentire una ragione per cui dovrebbe probabilmente essere una struttura. Non credo che OP apprezzerebbe i suggerimenti che modificano la semantica del resto del suo programma, specialmente quando non era nemmeno necessario risolvere il problema in questione. Non mi rendevo conto che le regole del compilatore fossero legali per alcuni ... forse sono sul sito Web sbagliato
Patrick Goley,

Risposte:


118

Pensa a cosa significa "ordinamento in base a criteri multipli". Significa che due oggetti vengono prima confrontati in base a un criterio. Quindi, se questi criteri sono gli stessi, i vincoli verranno interrotti dai criteri successivi e così via fino a ottenere l'ordinamento desiderato.

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
    }
}

Quello che vedi qui è il Sequence.sorted(by:)metodo , che consulta la chiusura fornita per determinare il confronto degli elementi.

Se il tuo ordinamento verrà utilizzato in molti posti, potrebbe essere meglio rendere il tuo tipo conforme al Comparable protocollo . In questo modo, puoi utilizzare il Sequence.sorted()metodo , che consulta l'implementazione Comparable.<(_:_:)dell'operatore per determinare il confronto tra gli elementi. In questo modo, è possibile ordinare qualsiasi Sequencedi Contacts senza mai dover duplicare il codice di smistamento.


2
Il elsecorpo deve essere compreso tra { ... }altrimenti il ​​codice non viene compilato.
Luca Angeletti

Fatto. Ho provato a implementarlo ma non sono riuscito a ottenere la sintassi corretta. Molte grazie.
sbkl

per sortvs. sortInPlacevedere qui . Vedi anche questo sotto, è molto più modulare
Honey

sortInPlaceNON è più disponibile in Swift 3, invece devi usarlo sort(). sort()muterà l'array stesso. Inoltre c'è una nuova funzione denominata sorted()che restituirà un array ordinato
Honey

2
@ AthanasiusOfAlex L'utilizzo ==non è una buona idea. Funziona solo per 2 proprietà. Non più di questo, e inizi a ripetere te stesso con molte espressioni booleane composte
Alexander - Reinstate Monica

119

Utilizzo delle tuple per confrontare più criteri

Un modo molto semplice per eseguire un ordinamento in base a criteri multipli (cioè ordinare in base a un confronto e, se equivalente, a un altro confronto) è usare le tuple , poiché gli operatori <e >hanno sovraccarichi per loro che eseguono confronti lessicografici.

/// 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

Per esempio:

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")
// ]

Questo confronterà lastNameprima le proprietà degli elementi . Se non sono uguali, l'ordinamento sarà basato su un <confronto con loro. Se sono uguali, si sposterà sulla successiva coppia di elementi nella tupla, ovvero confrontando le firstNameproprietà.

La libreria standard fornisce <e >sovraccarica le tuple da 2 a 6 elementi.

Se desideri diversi ordini di ordinamento per proprietà diverse, puoi semplicemente scambiare gli elementi nelle tuple:

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"),
// ]

Questo verrà ora ordinato in ordine lastNamediscendente, quindi firstNameascendente.


Definizione di un sort(by:)sovraccarico che accetta più predicati

Ispirato dalla discussione sull'ordinamento delle raccolte con mapchiusure e SortDescriptors , un'altra opzione potrebbe essere quella di definire un sovraccarico personalizzato di sort(by:)e sorted(by:)che si occupa di più predicati, in cui ogni predicato viene considerato a sua volta per decidere l'ordine degli elementi.

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
    }
  }
}

(Il secondPredicate:parametro è sfortunato, ma è necessario per evitare di creare ambiguità con l' sort(by:)overload esistente )

Questo poi ci permette di dire (usando l' contactsarray di prima):

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
  // ...
)

Sebbene il sito di chiamata non sia conciso come la variante della tupla, ottieni ulteriore chiarezza su cosa viene confrontato e in quale ordine.


Conforme a Comparable

Se farai regolarmente questo tipo di confronti, come suggeriscono @AMomchilov e @appzYourLife , puoi conformarti Contacta 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 ora chiedi solo sort()un ordine crescente:

contacts.sort()

o sort(by: >)per un ordine decrescente:

contacts.sort(by: >)

Definizione di ordinamenti personalizzati in un tipo nidificato

Se disponi di altri ordinamenti che desideri utilizzare, puoi definirli in un tipo nidificato:

extension Contact {
  enum Comparison {
    static let firstLastAscending: (Contact, Contact) -> Bool = {
      return ($0.firstName, $0.lastName) <
               ($1.firstName, $1.lastName)
    }
  }
}

e quindi chiama semplicemente come:

contacts.sort(by: Contact.Comparison.firstLastAscending)

contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) } Aiutato. Grazie
Prabhakar Kasi

Se, come me, le proprietà da ordinare sono optionals, allora si potrebbe fare qualcosa di simile: contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }.
BobCowe

Holly molly! Così semplice eppure così efficiente ... perché non ne ho mai sentito parlare ?! Molte grazie!
Ethenyl

@BobCowe Questo ti lascia in balia di come si ""confronta con altre stringhe (viene prima delle stringhe non vuote). È un po 'implicito, un po' magico e inflessibile se vuoi invece che la nils arrivi alla fine della lista. Ti consiglio di dare un'occhiata alla mia nilComparatorfunzione stackoverflow.com/a/44808567/3141234
Alexander - Reinstate Monica

18

Di seguito è mostrato un altro semplice approccio per l'ordinamento con 2 criteri.

Controlla il primo campo, in questo caso lo è lastName, se non sono uguali ordina per lastName, se lastNamesono uguali, quindi ordina per secondo campo, in questo caso firstName.

contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName  }

Ciò offre maggiore flessibilità rispetto alle tuple.
Babac

5

L'unica cosa che i tipi lessicografici non possono fare come descritto da @Hamish è gestire diverse direzioni di ordinamento, ad esempio ordina per primo campo discendente, il campo successivo ascendente, ecc.

Ho creato un post sul blog su come farlo in Swift 3 e mantengo il codice semplice e leggibile.

Potete trovare qui:

http://master-method.com/index.php/2016/11/23/sort-a-sequence-ie-arrays-of-objects-by-multiple-properties-in-swift-3/

Puoi anche trovare un repository GitHub con il codice qui:

https://github.com/jallauca/SortByMultipleFieldsSwift.playground

Il succo di tutto, ad esempio, se hai un elenco di posizioni, sarai in grado di farlo:

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
        )

1
"L'unica cosa che i tipi lessicografici non possono fare come descritto da @Hamish è gestire diverse direzioni di ordinamento" - sì, possono semplicemente scambiare gli elementi nelle tuple;)
Hamish

Trovo questo un esercizio teorico interessante ma molto più complicato della risposta di @ Hamish. Meno codice è codice migliore secondo me.
Manuel

5

Questa domanda ha già molte ottime risposte, ma desidero indicare un articolo: Ordina descrittori in Swift . Abbiamo diversi modi per eseguire l'ordinamento di più criteri.

  1. Utilizzando NSSortDescriptor, in questo modo ha alcune limitazioni, l'oggetto dovrebbe essere una classe ed eredita da 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),
    ]

    Qui, ad esempio, vogliamo ordinare per cognome, poi per nome, infine per anno di nascita. E vogliamo farlo senza distinzione tra maiuscole e minuscole e utilizzando le impostazioni locali dell'utente.

    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. Utilizzo del modo rapido di ordinare con cognome / nome. In questo modo dovrebbe funzionare sia con class / struct. Tuttavia, non ordiniamo per yearOfBirth qui.

    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. Modo rapido per inmitate NSSortDescriptor. Questo utilizza il concetto che "le funzioni sono un tipo di prima classe". SortDescriptor è un tipo di funzione, accetta due valori, restituisce un valore bool. Diciamo sortByFirstName prendiamo due parametri ($ 0, $ 1) e confrontiamo i loro nomi. Le funzioni di combinazione prendono un mucchio di SortDescriptors, li confrontano tutti e danno ordini.

    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)]

    Questo è positivo perché puoi usarlo sia con struct che con class, puoi persino estenderlo per confrontarlo con nils.

Tuttavia, la lettura dell'articolo originale è fortemente suggerita. Ha molti più dettagli e ben spiegato.


2

Consiglierei di utilizzare la soluzione tupla di Hamish poiché non richiede codice aggiuntivo.


Se vuoi qualcosa che si comporti come ifistruzioni ma semplifichi la logica di ramificazione, puoi usare questa soluzione, che ti permette di fare quanto segue:

animals.sort {
  return comparisons(
    compare($0.family, $1.family, ascending: false),
    compare($0.name, $1.name))
}

Ecco le funzioni che ti consentono di farlo:

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
}

Se vuoi provarlo, puoi usare questo codice extra:

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 differenza rispetto alla soluzione di Jamie è che l'accesso alle proprietà è definito in linea piuttosto che come metodi statici / istanza sulla classe. Ad esempio $0.familyinvece di Animal.familyCompare. E l'ascendente / discendente è controllato da un parametro invece che da un operatore sovraccarico. La soluzione di Jamie aggiunge un'estensione su Array mentre la mia soluzione utilizza il metodo integrato sort/ sortedma richiede la definizione di due ulteriori: comparee comparisons.

Per completezza, ecco come la mia soluzione si confronta con la soluzione della tupla di Hamish . Per dimostrare userò un esempio selvaggio in cui vogliamo ordinare le persone in base (name, address, profileViews)alla soluzione di Hamish valuterà ciascuno dei 6 valori di proprietà esattamente una volta prima che inizi il confronto. Questo potrebbe non essere desiderato o non essere desiderato. Ad esempio, supponendo che profileViewssia una chiamata di rete costosa, potremmo voler evitare di chiamare a profileViewsmeno che non sia assolutamente necessario. La mia soluzione eviterà di valutare profileViewsfino a $0.name == $1.namee $0.address == $1.address. Tuttavia, quando valuta profileViews, probabilmente valuterà molte più volte di una volta.


1

Che ne dite di:

contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }

lexicographicallyPrecedesrichiede che tutti i tipi nell'array siano gli stessi. Ad esempio [String, String]. Quello che probabilmente OP vuole è mescolare e abbinare i tipi: [String, Int, Bool]così potrebbero fare [$0.first, $0.age, $0.isActive].
Senseful

-1

ha funzionato per il mio array [String] in Swift 3 e sembra che in Swift 4 sia ok

array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}

Hai letto la domanda prima di rispondere? Ordina per più parametri, non uno, ciò che presenti.
Vive
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.