117 votes

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

J'ai un tableau d'objets Contact:

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

Classe Contact:

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

Et je voudrais trier ce tableau par lastName puis par firstName dans le cas où certains contacts ont le même lastName.

Je peux trier par l'un de ces critères, mais pas par les deux.

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

Comment pourrais-je ajouter d'autres critères pour trier ce tableau?

2 votes

Faites exactement comme vous venez de le dire! Votre code à l'intérieur des accolades doit dire: "Si les noms de famille sont identiques, alors triez par prénom; sinon, triez par nom de famille".

4 votes

Je vois quelques odeurs de code ici: 1) Contact ne devrait probablement pas hériter de NSObject, 2) Contact devrait probablement être une struct, et 3) firstName et lastName ne devraient probablement pas être des optionnels déballés implicitement.

3 votes

@AMomchilov Il n'y a aucune raison de suggérer que Contact devrait être une structure car vous ne savez pas si le reste de son code repose déjà sur la sémantique de référence en utilisant des instances de celle-ci.

164voto

Hamish Points 42073

Utilisation des tuples pour comparer plusieurs critères

Une façon vraiment simple d'effectuer un tri selon plusieurs critères (c'est-à-dire trier selon une comparaison, et si équivalents, alors selon une autre comparaison) est d'utiliser des tuples, car les opérateurs < et > ont des surcharges pour eux qui effectuent des comparaisons lexicographiques.

/// Renvoie une valeur booléenne indiquant si le premier tuple est ordonné
/// avant le deuxième dans un ordre lexicographique.
///
/// Étant donné deux tuples `(a1, a2, ..., aN)` et `(b1, b2, ..., bN)`, le premier
/// tuple est avant le deuxième tuple si et seulement si
/// `a1 < b1` ou (`a1 == b1` et
/// `(a2, ..., aN) < (b2, ..., bN)`).
public func < (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'abord les propriétés lastName des éléments. S'ils ne sont pas égaux, alors l'ordre de tri sera basé sur une comparaison avec < entre eux. S'ils sont égaux, alors cela passera au prochain couple d'éléments dans le tuple, c'est-à-dire en comparant les propriétés firstName.

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

Si vous voulez des ordres de tri différents pour différentes propriétés, vous pouvez simplement échanger 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 triera maintenant par lastName descendant, puis par firstName ascendant.


Définition d'une surcharge de sort(by:) qui prend plusieurs prédicats

En s'inspirant de la discussion sur Trier les collections avec des map closures et des SortDescriptors, une autre option serait de définir une surcharge personnalisée de sort(by:) et sorted(by:) qui traite de multiples prédicats - où chaque prédicat est considéré tour à 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 {
  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, ...

(Le paramètre secondPredicate: est malheureux, mais il est nécessaire pour éviter de créer des ambiguïtés avec la surcharge existante de sort(by:))

Ensuite, nous pouvons dire (en utilisant le tableau contacts précédent):

contacts.sort(by:
  { $0.lastName > $1.lastName },  // d'abord trier par lastName descendant
  { $0.firstName < $1.firstName } // ... puis par firstName ascendant
  // ...
)

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

// ou avec sorted(by:)...
let sortedContacts = contacts.sorted(by:
  { $0.lastName > $1.lastName },  // d'abord trier par lastName descendant
  { $0.firstName < $1.firstName } // ... puis par firstName ascendant
  // ...
)

Bien que le site d'appel ne soit pas aussi concis que la variante de tuple, vous gagnez en clarté supplémentaire sur ce qui est comparé et dans quel ordre.


Conformité à Comparable

Si vous prévoyez de faire ce genre de comparaisons régulièrement alors, comme le suggèrent @AMomchilov & @appzYourLife, vous pouvez faire en sorte que Contact soit conforme à 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 il suffit d'appeler sort() pour un ordre ascendant:

contacts.sort()

ou sort(by: >) pour un ordre descendant:

contacts.sort(by: >)

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

Si vous avez d'autres ordres de tri que vous souhaitez utiliser, 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)

0 votes

contacts.sort { ($0.nom, $0.prénom) < ($1.nom, $1.prénom) } A aidé. Merci

0 votes

Si, comme moi, les propriétés à trier sont optionnelles, vous pourriez faire quelque chose comme ceci : contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }.

1 votes

Holly molly! Si simple mais tellement efficace... pourquoi n'ai-je jamais entendu parler de ça?! Merci beaucoup!

149voto

XAleXOwnZX Points 159

Pensez à ce que signifie "tri par critères multiples". Cela signifie que deux objets sont d'abord comparés selon un critère. Ensuite, si ces critères sont les mêmes, les liens seront résolus par le critère suivant, et ainsi de suite jusqu'à obtenir l'ordonnancement souhaité.

let sortedContacts = contacts.sort {
    if $0.lastName != $1.lastName { // tout d'abord, comparer par noms de famille
        return $0.lastName < $1.lastName
    }
    /*  noms de famille identiques, résoudre les liens par foo
    else if $0.foo != $1.foo {
        return $0.foo < $1.foo
    }
    ... répéter pour tous les autres champs dans le tri
    */
    else { // Tous les autres champs sont liés, résoudre les liens par prénom
        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 sera utilisé à de nombreux endroits, il peut être préférable de faire en sorte que votre type soit conforme au protocole Comparable. De cette façon, vous pouvez utiliser la méthode Sequence.sorted(), qui consulte votre implémentation de l'opérateur Comparable.<(_:_:) pour déterminer comment les éléments se comparent. De cette manière, vous pouvez trier n'importe quelle Séquence de Contacts sans jamais avoir à dupliquer le code de tri.

3 votes

Le corps du else doit être entre { ... } sinon le code ne se compile pas.

0 votes

Compris. J'ai essayé de l'implémenter mais je n'ai pas réussi à obtenir la syntaxe correcte. Merci beaucoup.

0 votes

Pour sort vs. sortInPlace voir ici. Voir également ici ci-dessous, c'est beaucoup plus modulaire

27voto

oyalhi Points 2082

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

Vérifiez le premier champ, dans ce cas c'est lastName, s'ils ne sont pas égaux, triez par lastName, si les lastName sont égaux, alors triez par le deuxième champ, dans ce cas firstName.

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

1 votes

Cela offre plus de flexibilité que les tuples.

1 votes

J'aime le fait que c'est si compact. J'ai ajouté quelques commentaires, donc si je reviens au code après un certain temps, je comprendrai la logique derrière. Mais c'est probablement juste moi...

0 votes

Ceci est une réponse dupliquée avec celle acceptée: utiliser l'opérateur ternaire ou l'instruction if-else ne fait aucune différence - la question ne concerne pas la syntaxe.

7voto

XueYu Points 1108

Cette question a déjà de nombreuses réponses intéressantes, mais je tiens à souligner un article - Sort Descriptors in Swift. Nous avons plusieurs façons de trier selon plusieurs critères.

  1. En utilisant NSSortDescriptor, cette méthode a quelques limitations, l'objet doit être une classe et hériter 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 de manière insensible à la casse et en utilisant le paramètre local 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. En utilisant la méthode de tri Swift avec le nom de famille/prénom. Cette méthode devrait fonctionner avec les classes/structs. Cependant, nous ne trions pas 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)]
  3. Méthode Swift pour imiter NSSortDescriptor. Cela utilise le concept selon lequel les 'fonctions sont un type de première classe'. SortDescriptor est un type de fonction, prend deux valeurs, renvoie un booléen. Disons que pour trier par prénom, nous prenons deux paramètres ($0, $1) et comparons leurs prénoms. La fonction de combinaison prend un groupe de SortDescriptors, les compare tous et donne des ordres.

    typealias SortDescriptor = (Value, Value) -> Bool
    
    let sortByFirstName: SortDescriptor = {
        $0.first.localizedCaseInsensitiveCompare($1.first) == .orderedAscending
    }
    let sortByYear: SortDescriptor = { $0.yearOfBirth < $1.yearOfBirth }
    let sortByLastName: SortDescriptor = {
        $0.last.localizedCaseInsensitiveCompare($1.last) == .orderedAscending
    }
    
    func combine
        (sortDescriptors: [SortDescriptor]) -> SortDescriptor {
        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 = 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 avec des struct et des classes, vous pouvez même l'étendre pour comparer avec des nil.

Cependant, il est fortement recommandé de lire l'article original. Il contient beaucoup plus de détails et est bien expliqué.

5voto

Jaime Allauca Points 51

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, par exemple trier par le premier champ en ordre décroissant, le champ suivant en ordre croissant, etc.

J'ai créé un article de blog sur la manière de le faire en 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-i-e-arrays-of-objects-by-multiple-properties-in-swift-3/

Vous pouvez également trouver un dépôt GitHub avec le code ici:

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

L'essentiel de tout cela, par exemple, si vous avez une liste de lieux, vous pourrez faire ceci:

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 votes

"La seule chose que les tris lexicographiques ne peuvent pas faire telle que décrite par @Hamish est de gérer différentes directions de tri" - oui ils le peuvent, il suffit d'échanger les éléments dans les tuples ;)

0 votes

Je trouve que c'est un exercice théorique intéressant mais beaucoup plus compliqué que la réponse de @Hamish. Moins de code, c'est un meilleur code à mon avis.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X