Swift - Sort array of objects with multiple criteria

后端 未结 8 958
北荒
北荒 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 12:11

    The one thing the lexicographical sorts cannot do as described by @Hamish is to handle different sorting directions, say sort by the first field descending, the next field ascending, etc.

    I created a blog post on how to this in Swift 3 and keep the code simple and readable.

    You can find it here:

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

    You can also find a GitHub repository with the code here:

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

    The gist of it all, say, if you have list of locations, you will be able to do this:

    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
            )
    
    0 讨论(0)
  • 2020-11-22 12:19

    Using tuples to do a comparison of multiple criteria

    A really simple way of performing a sort by multiple criteria (i.e sorting by one comparison, and if equivalent, then by another comparison) is by using tuples, as the < and > operators have overloads for them that perform lexicographic comparisons.

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

    For example:

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

    This will compare the elements' lastName properties first. If they aren't equal, then the sort order will be based on a < comparison with them. If they are equal, then it will move onto the next pair of elements in the tuple, i.e comparing the firstName properties.

    The standard library provides < and > overloads for tuples of 2 to 6 elements.

    If you want different sorting orders for different properties, you can simply swap the elements in the 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"),
    // ]
    

    This will now sort by lastName descending, then firstName ascending.


    Defining a sort(by:) overload that takes multiple predicates

    Inspired by the discussion on Sorting Collections with map closures and SortDescriptors, another option would be to define a custom overload of sort(by:) and sorted(by:) that deals with multiple predicates – where each predicate is considered in turn to decide the order of the elements.

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

    (The secondPredicate: parameter is unfortunate, but is required in order to avoid creating ambiguities with the existing sort(by:) overload)

    This then allows us to say (using the contacts array from earlier):

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

    Although the call-site isn't as concise as the tuple variant, you gain additional clarity with what's being compared and in what order.


    Conforming to Comparable

    If you're going to be doing these kinds of comparisons regularly then, as @AMomchilov & @appzYourLife suggest, you can conform Contact to 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)
      }
    }
    

    And now just call sort() for an ascending order:

    contacts.sort()
    

    or sort(by: >) for a descending order:

    contacts.sort(by: >)
    

    Defining custom sort orders in a nested type

    If you have other sort orders you want use, you can define them in a nested type:

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

    and then simply call as:

    contacts.sort(by: Contact.Comparison.firstLastAscending)
    
    0 讨论(0)
提交回复
热议问题