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?
In Swift, we can also sort arrays in ascending and descending order. To sort the array we use the sort() function. This function is used to sort the elements of the array in a specified order either in ascending order or in descending order.
To sort an integer array in increasing order in Swift, call sort() method on this array. sort() method sorts this array in place, and by default, in ascending order.
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.
sort(by:)
overload that takes multiple predicatesInspired 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.
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: >)
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)
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 Contact
s without ever having to duplicate the sorting code.
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 }
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.
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)]
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)]
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With