web-dev-qa-db-fra.com

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

J'ai un tableau d'objets Contact

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 ensuite par firstName au cas où certains contacts auraient le même lastName.

Je suis capable de 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?

51
sbkl

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

Si votre tri doit être utilisé à de nombreux endroits, il peut être préférable de rendre votre type conforme au protocole Comparable . De cette façon, vous pouvez utiliser Sequence.sorted(), méthode , qui consulte votre implémentation de l'opérateur Comparable.<(_:_:) pour déterminer la comparaison des éléments. De cette façon, vous pouvez trier n'importe quelle Sequence de Contacts sans avoir à dupliquer le code de tri.

68
Alexander

Utiliser des n-uplets pour comparer plusieurs critères

Un moyen très simple d'effectuer un tri selon plusieurs critères (c'est-à-dire le tri par comparaison et, le cas échéant, par comparaison) consiste à utiliser tuples, car les opérateurs < et > disposent de surcharges permettant d'effectuer 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 les propriétés lastName des éléments en premier. S'ils ne sont pas égaux, l'ordre de tri sera basé sur une comparaison de < avec eux. S'ils sont égaux, il passera à la paire d'éléments suivante dans le nuplet, c'est-à-dire en comparant les propriétés firstName.

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

Si vous voulez différents ordres de tri pour différentes propriétés, vous pouvez simplement échanger les éléments dans les n-uplets:

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 firstName croissant.


Définir une surcharge sort(by:) prenant plusieurs prédicats

Inspiré par la discussion sur Tri des collections avec map fermetures 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 - chaque prédicat étant pris en compte 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 paramètre secondPredicate: est regrettable, mais il est nécessaire pour éviter de créer des ambiguïtés avec la surcharge sort(by:) existante.)

Cela nous permet ensuite de dire (en utilisant le tableau contacts 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 obtenez une clarté supplémentaire avec ce qui est comparé et dans quel ordre.


Conforme à Comparable

Si vous comptez effectuer ce type de comparaison régulièrement, comme le suggère @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éfinir des 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)
    }
  }
}

et ensuite simplement appeler comme:

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

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

Recherchez le premier champ, dans ce cas, il s'agit de lastName, s'ils ne sont pas égaux, triez par lastName, si lastName sont égaux, 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  }
7
oyalhi

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

  1. En utilisant NSSortDescriptor, cette manière a certaines 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 de famille, puis prénom, enfin par année de naissance. Et nous voulons le faire sans distinction de 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 de tri rapide avec nom de famille/prénom ..__ Cette méthode devrait fonctionner avec les deux classes/struct. Cependant, nous ne faisons pas de tri 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. Manière rapide d'introduire NSSortDescriptor. Ceci utilise le concept que "les fonctions sont un type de première classe". SortDescriptor est un type de fonction, prend deux valeurs, retourne un bool. Disons 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 parce que vous pouvez l'utiliser à la fois avec struct et class, vous pouvez même l'étendre pour le comparer avec nils.

Néanmoins, la lecture de l’article original est fortement recommandée. Il a beaucoup plus de détails et bien expliqué.

5
XueYu

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 les instructions if mais simplifie la logique de branchement, vous pouvez utiliser cette solution, ce 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 ceci:

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 voulez 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 qu'en tant que méthode statique/instance sur la classe. Par exemple. $0.family au lieu de Animal.familyCompare. Et ascendant/descendant est contrôlé par un paramètre plutôt que par un opérateur surchargé. La solution de Jamie ajoute une extension à Array alors que ma solution utilise la méthode sort/sorted intégrée, mais nécessite la définition de deux méthodes supplémentaires: compare et comparisons.

Par souci d'exhaustivité, voici comment ma solution se compare à la solution de Hamish's Tuple . Pour illustrer mon propos, je vais utiliser un exemple sauvage dans lequel nous voulons trier les personnes 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 ou ne pas être désiré. Par exemple, en supposant que profileViews est un appel réseau coûteux, nous voudrons peut-être éviter d'appeler profileViews à moins que ce ne soit absolument nécessaire. Ma solution évitera d’évaluer profileViews jusqu’à $0.name == $1.name et $0.address == $1.address. Cependant, quand il évaluera profileViews, il en évaluera probablement beaucoup plus de fois.

2
Senseful

Que diriez-vous: 

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

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

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