Swift - Trier un tableau d'objets avec plusieurs critères

91

J'ai un tableau d' Contactobjets:

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 puis par firstNameau cas où certains contacts la même chose lastName.

Je peux 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?

sbkl
la source
2
Faites-le exactement de la même manière que vous venez de le dire! Votre code à l'intérieur des accolades devrait dire: "Si les noms de famille sont identiques, triez par prénom; sinon, triez par nom de famille".
matt
4
Je vois quelques odeurs de code ici: 1) Contactne devrait probablement pas hériter de NSObject, 2) Contactdevrait probablement être une structure, et 3) firstNameet lastNamene devrait probablement pas être des options implicitement déballées.
Alexander - Réintégrer Monica le
3
@AMomchilov Il n'y a aucune raison de suggérer que Contact devrait être une structure parce que vous ne savez pas si le reste de son code repose déjà sur la sémantique de référence lors de l'utilisation de ses instances.
Patrick Goley
3
@AMomchilov "Probablement" est trompeur parce que vous ne savez exactement rien sur le reste de la base de code. S'il est changé en structure, toutes les copies soudaines sont générées lors de la mutation des variables, au lieu de modifier l'instance en cours. Il s'agit d'un changement radical de comportement et cela entraînerait "probablement" des bogues car il est peu probable que tout ait été correctement codé pour la sémantique de référence et de valeur.
Patrick Goley
1
@AMomchilov Je n'ai pas encore entendu une raison pour laquelle il devrait probablement s'agir d'une structure. Je ne pense pas qu'OP apprécierait les suggestions qui modifient la sémantique du reste de son programme, surtout quand il n'était même pas nécessaire de résoudre le problème en question. Je n'ai pas réalisé que les règles du compilateur étaient légales pour certains ... peut-être que je suis sur le mauvais site Web
Patrick Goley

Réponses:

118

Pensez à ce que signifie «tri selon plusieurs critères». Cela signifie que deux objets sont d'abord comparés par un critère. Ensuite, si ces critères sont les mêmes, les liens seront rompus par 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 Sequence.sorted(by:)méthode , qui consulte la fermeture fournie pour déterminer comment les éléments se comparent.

Si votre tri est utilisé dans de nombreux endroits, il peut être préférable de rendre votre type conforme au Comparable protocole . De cette façon, vous pouvez utiliser Sequence.sorted()method , qui consulte votre implémentation de l' Comparable.<(_:_:)opérateur pour déterminer comment les éléments se comparent. De cette façon, vous pouvez trier n'importe lequel Sequencedes Contacts sans jamais avoir à dupliquer le code de tri.

Alexander - Réintégrer Monica
la source
2
Le elsecorps doit être entre, { ... }sinon le code ne se compile pas.
Luca Angeletti
Je l'ai. J'ai essayé de l'implémenter mais je n'ai pas réussi à obtenir la syntaxe correcte. Merci beaucoup.
sbkl
pour sortvs sortInPlacevoir ici . Voir aussi ceci ci - dessous, c'est beaucoup plus modulaire
Honey
sortInPlacen'est plus disponible dans Swift 3, vous devez plutôt l'utiliser sort(). sort()fera muter le tableau lui-même. Il existe également une nouvelle fonction nommée sorted()qui retournera un tableau trié
Honey
2
@AthanasiusOfAlex Utiliser ==n'est pas une bonne idée. Cela ne fonctionne que pour 2 propriétés. Pas plus que cela, et vous commencez à vous répéter avec beaucoup d'expressions booléennes composées
Alexander - Réintégrer Monica
119

Utilisation de tuples pour faire une comparaison de plusieurs critères

Un moyen très simple d'effectuer un tri selon plusieurs critères (c'est-à-dire le tri par une comparaison, et si équivalent, puis par une autre comparaison) est d'utiliser des tuples , car les opérateurs <et >ont des surcharges pour eux qui effectuent 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 d' lastNameabord les propriétés des éléments . S'ils ne sont pas égaux, l'ordre de tri sera basé sur une <comparaison avec eux. S'ils sont égaux, alors il passera à la prochaine paire d'éléments dans le tuple, c'est-à-dire en comparant les firstNamepropriétés.

La bibliothèque standard fournit <et >surcharge les tuples de 2 à 6 éléments.

Si vous voulez différents ordres de tri pour différentes propriétés, vous pouvez simplement permuter les éléments dans les tuples:

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 firstNamecroissant.


Définir un sort(by:) surcharge qui prend plusieurs prédicats

Inspiré par la discussion sur le tri des collections avec mapfermetures 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 - où chaque prédicat est considéré à son tour 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 secondPredicate:paramètre est malheureux, mais est nécessaire pour éviter de créer des ambiguïtés avec la sort(by:)surcharge existante )

Cela nous permet alors de dire (en utilisant le contactstableau 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 gagnez en clarté supplémentaire avec ce qui est comparé et dans quel ordre.


Se conformer à Comparable

Si vous comptez faire ce genre de comparaisons régulièrement, comme le suggèrent @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: >)

Définition d'ordres de tri personnalisés dans un type imbriqué

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

puis appelez simplement comme:

contacts.sort(by: Contact.Comparison.firstLastAscending)
Hamish
la source
contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) } Aidé. Merci
Prabhakar Kasi
Si comme moi, les propriétés à trier sont optionals, alors vous pourriez faire quelque chose comme ceci: contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }.
BobCowe
Holly Molly! Si simple mais si efficace ... pourquoi je n'ai jamais entendu parler de ça?! Merci beaucoup!
Ethenyl
@BobCowe Cela vous laisse à la merci de la ""comparaison avec d'autres chaînes (il vient avant les chaînes non vides). C'est un peu implicite, un peu magique et inflexible si vous voulez que les nils viennent plutôt à la fin de la liste. Je vous recommande de jeter un œil à ma nilComparatorfonction stackoverflow.com/a/44808567/3141234
Alexander - Réintégrer Monica
18

Une autre approche simple pour le tri avec 2 critères est présentée ci-dessous.

Vérifiez le premier champ, dans ce cas c'est le cas lastName, s'ils ne sont pas égaux, triez par lastName, si lastNameles valeurs sont égales, 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  }
oyalhi
la source
Cela donne plus de flexibilité que les tuples.
Babac le
5

La seule chose que les tris lexicographiques ne peuvent pas faire comme décrit par @Hamish est de gérer différentes directions de tri, disons le tri par le premier champ décroissant, le champ suivant croissant, etc.

J'ai créé un article de blog sur la façon de procéder dans Swift 3 et de garder le code simple et lisible.

Vous pouvez le trouver ici:

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

Vous pouvez également trouver un référentiel GitHub avec le code ici:

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

L'essentiel de tout cela, par exemple, si vous avez une liste d'emplacements, vous pourrez le faire:

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
la source
1
"La seule chose que les sortes lexicographiques ne peuvent pas faire comme décrit par @Hamish est de gérer différentes directions de tri" - oui, ils peuvent, il suffit d'échanger les éléments dans les tuples;)
Hamish
Je trouve cela un exercice théorique intéressant mais beaucoup plus compliqué que la réponse de @ Hamish. Moins de code est un meilleur code à mon avis.
Manuel
5

Cette question a déjà de nombreuses réponses intéressantes, mais je veux pointer vers un article - Sort Descriptors in Swift . Nous avons plusieurs façons de faire le tri selon plusieurs critères.

  1. En utilisant NSSortDescriptor, cette façon a quelques 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, puis prénom, enfin par année de naissance. Et nous voulons le faire sans tenir compte de la 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)]
  2. Utilisation de la méthode Swift pour trier par nom / prénom. Cette méthode devrait fonctionner avec les deux class / struct. Cependant, nous ne trions pas par yearOfBirth 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)]
  3. Manière rapide d'inmiter NSSortDescriptor. Cela utilise le concept que «les fonctions sont un type de première classe». SortDescriptor est un type de fonction, prend deux valeurs, renvoie un booléen. Dites 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 car vous pouvez l'utiliser à la fois avec struct et class, vous pouvez même l'étendre pour le comparer avec nils.

Pourtant, la lecture de l' article original est fortement suggérée. Il a beaucoup plus de détails et bien expliqué.

XueYu
la source
2

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 des ifinstructions mais simplifie la logique de branchement, vous pouvez utiliser cette solution, 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 cela:

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 souhaitez 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 que comme des méthodes statiques / d'instance sur la classe. Par exemple $0.familyau lieu de Animal.familyCompare. Et ascendant / descendant est contrôlé par un paramètre au lieu d'un opérateur surchargé. La solution de Jamie ajoute une extension sur Array alors que ma solution utilise la méthode intégrée sort/ sortedmais nécessite la définition de deux autres: compareet comparisons.

Par souci d'exhaustivité, voici comment ma solution se compare à la solution tuple de Hamish . Pour démontrer, je vais utiliser un exemple sauvage où nous voulons trier les gens 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 souhaité ou non. Par exemple, en supposant qu'il profileViewss'agit d'un appel réseau coûteux, nous pouvons éviter d'appelerprofileViews moins que ce ne soit absolument nécessaire. Ma solution évitera d'évaluer profileViewsjusqu'à $0.name == $1.nameet $0.address == $1.address. Cependant, lorsqu'il sera évalué, profileViewsil évaluera probablement plusieurs fois une fois.

Sensé
la source
1

Que diriez-vous:

contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
Lou Zell
la source
lexicographicallyPrecedesexige que tous les types du tableau soient identiques. Par exemple [String, String]. Ce que OP veut probablement, c'est mélanger et assortir les types: [String, Int, Bool]ainsi ils pourraient le faire [$0.first, $0.age, $0.isActive].
Senseful
-1

cela a fonctionné pour mon tableau [String] dans Swift 3 et il semble que dans Swift 4 est ok

array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}
mamaz
la source
Avez-vous lu la question avant de répondre? Trier par plusieurs paramètres, pas un, ce que vous présentez.
Vive le