134

I have an array of Contact objects:

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

Contact class:

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

And I would like to sort that array by lastName and then by firstName in case some contacts got the same lastName.

I'm able to sort by one of those criteria, but not both.

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

How could I add more criteria to sort this array?

7
  • 2
    Do it exactly the same way you just said! Your code inside the curly braces should say: "If the last names are the same, then sort by first name; otherwise sort by last name".
    – matt
    Commented Jun 3, 2016 at 0:04
  • 4
    I see a few code smells here: 1) Contact probably shouldn't inherit from NSObject, 2) Contact should probably be a struct, and 3) firstName and lastName probably shouldn't be implicitly unwrapped optionals.
    – Alexander
    Commented Jun 3, 2016 at 0:16
  • 3
    @AMomchilov There's no reason to suggest Contact should be a struct because you don't know if the rest of his code already relies on reference semantics in using instances of it. Commented Jun 3, 2016 at 0:24
  • 1
    @PatrickGoley "...probably..."
    – Alexander
    Commented Jun 3, 2016 at 2:00
  • 4
    @AMomchilov "Probably" is misleading because you know exactly nothing about the rest of the codebase. If it is changed to a struct, all of the sudden copies are generated when mutating the vars, instead of modifying the instance at hand. This is a drastic change in behavior and doing that would "probably" result in bugs because it's unlikely everything has been code properly for both reference and value semantics. Commented Jun 3, 2016 at 2:09

8 Answers 8

196

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 {
  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)
5
  • contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) } Helped. Thanks Commented Jan 29, 2019 at 19:43
  • If like me, the properties to be sorted are optionals, then you could do something like this: contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }.
    – BobCowe
    Commented Sep 19, 2019 at 21:13
  • 1
    Holly molly! So simple yet so efficient... why I've never heard of that?! Thanks a lot!
    – Ethenyl
    Commented Dec 30, 2019 at 14:08
  • @BobCowe That leaves you at the mercy of how "" compares to other strings (it comes before non-empty strings). It's kind of implicit, kinda magic, and inflexible if you want the nils to come at the end of the list instead. I recommend you take a look at my nilComparator function stackoverflow.com/a/44808567/3141234
    – Alexander
    Commented May 3, 2020 at 15:44
  • What an outstanding approach that Tuple approach is. Kudos. Commented Jun 5 at 4:06
161

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.

10
  • 3
    The else body must be between { ... } otherwise the code doesn't compile. Commented Jun 3, 2016 at 0:18
  • Got it. I tried to implement it but couldn't get the syntax right. Thanks a lot.
    – sbkl
    Commented Jun 3, 2016 at 0:18
  • for sort vs. sortInPlace see here. Aslo see this below, it's much more modular
    – mfaani
    Commented Dec 19, 2016 at 15:11
  • 2
    @AthanasiusOfAlex Using == is not a good idea. It only works for 2 properties. Any more than that, and you start repeating yourself with a lot of compounded boolean expressions
    – Alexander
    Commented Apr 16, 2017 at 23:23
  • 1
    guys the perfect example of wher eyou can use breakaway code. there's no need at all for the elses! if it returns, it returns !
    – Fattie
    Commented Mar 12, 2020 at 12:09
32

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  }
4
  • 1
    This gives more flexibility than tuples.
    – Babac
    Commented Jun 4, 2020 at 16:09
  • 1
    I like the fact that it's so compact. I did add some comments, so if I come back to the code after quite a while I will understand the logic behind it. But that's probably just me...
    – Carl Smith
    Commented Dec 12, 2020 at 8:54
  • This is a duplicate answer with the accepted one: using the ternary operator or the if-else statement doesn't make any difference – the question is not about syntax. Commented Jan 25, 2022 at 6:37
  • For me, this is the quickest/easiest to read and understand Commented Apr 18 at 8:52
7

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
5

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
        )
2
  • 1
    "The one thing the lexicographical sorts cannot do as described by @Hamish is to handle different sorting directions" – yes they can, just swap the elements in the tuples ;)
    – Hamish
    Commented Apr 20, 2017 at 17:44
  • I find this an interesting theoretical exercise but much more complicated than @Hamish's answer. Less code is better code in my opinion.
    – Manuel
    Commented Oct 22, 2017 at 5:25
3

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.

1

How about:

contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
1
  • lexicographicallyPrecedes requires all types in the array to be the same. For example [String, String]. What OP probably wants is to mix and match types: [String, Int, Bool] so they could do [$0.first, $0.age, $0.isActive].
    – Senseful
    Commented Apr 20, 2017 at 17:10
-5

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}
1
  • Did you read the question before answering? Sort by multiple parameters, not one, what you present.
    – Nat
    Commented Sep 9, 2018 at 10:34

Not the answer you're looking for? Browse other questions tagged or ask your own question.