Swift - Sort array of objects with multiple criteria

后端 未结 8 936
北荒
北荒 2020-11-22 11:19

I have an array of Contact objects:

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

Contact class:

Class Contact:NSOBjec         


        
相关标签:
8条回答
  • 2020-11-22 11:57

    I'd recommend using Hamish's tuple solution since it doesn't require extra code.


    If you want something that behaves like if statements but simplifies the branching logic, you can use this solution, which allows you to do the following:

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

    Here are the functions that allow you to do this:

    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
    }
    

    If you want to test it out, you can use this extra code:

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

    The main differences from Jamie's solution is that the access to the properties are defined inline rather than as static/instance methods on the class. E.g. $0.family instead of Animal.familyCompare. And ascending/descending is controlled by a parameter instead of an overloaded operator. Jamie's solution adds an extension on Array whereas my solution uses the built in sort/sorted method but requires two additional ones to be defined: compare and comparisons.

    For completeness sake, here's how my solution compares to the Hamish's tuple solution. To demonstrate I'll use a wild example where we want to sort people by (name, address, profileViews) Hamish's solution will evaluate each of the 6 property values exactly once before the comparison begins. This may not or may not be desired. For example, assuming profileViews is an expensive network call we may want to avoid calling profileViews unless it's absolutely necessary. My solution will avoid evaluating profileViews until $0.name == $1.name and $0.address == $1.address. However, when it does evaluate profileViews it'll likely evaluate many more times than once.

    0 讨论(0)
  • 2020-11-22 12:01

    that worked for my array[String] in Swift 3 and it seems in Swift 4 is ok

    array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}
    
    0 讨论(0)
  • 2020-11-22 12:03

    Another simple approach for sorting with 2 criteria is shown below.

    Check for the first field, in this case it is lastName, if they are not equal sort by lastName, if lastName's are equal, then sort by the second field, in this case firstName.

    contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName  }
    
    0 讨论(0)
  • 2020-11-22 12:03

    This question has already many great answers, but I want to point to an article - Sort Descriptors in Swift. We have several ways to do the multiple criteria sorting.

    1. Using NSSortDescriptor, this way has some limitations, the object should be a class and inherits from 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),
      ]
      

      Here, for example, we want to sort by last name, then first name, finally by birth year. And we want do it case insensitively and using the user’s locale.

      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. Using Swift way of sorting with last name/first name . This way should work with both class/struct. However, we don't sort by yearOfBirth here.

      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. Swift way to inmitate NSSortDescriptor. This uses the concept that 'functions are a first-class type'. SortDescriptor is a function type, takes two values, returns a bool. Say sortByFirstName we take two parameters($0,$1) and compare their first names. The combine functions takes a bunch of SortDescriptors, compare all of them and give orders.

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

      This is good because you can use it with both struct and class, you can even extend it to compare with nils.

    Still, reading the original article is strongly suggested. It has much more details and well explained.

    0 讨论(0)
  • 2020-11-22 12:09

    How about:

    contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
    
    0 讨论(0)
  • 2020-11-22 12:10

    Think of what "sorting by multiple criteria" means. It means that two objects are first compared by one criteria. Then, if those criteria are the same, ties will be broken by the next criteria, and so on until you get the desired ordering.

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

    What you're seeing here is the Sequence.sorted(by:) method, which consults the provided closure to determine how elements compare.

    If your sorting will be used in many places, it may be better to make your type conform to the Comparable protocol. That way, you can use Sequence.sorted() method, which consults your implementation of the Comparable.<(_:_:) operator to determine how elements compare. This way, you can sort any Sequence of Contacts without ever having to duplicate the sorting code.

    0 讨论(0)
提交回复
热议问题