Sort array of objects by multiple properties with Swift Tuple
Table of Contents
In my last article, How to sort by multiple properties in Swift, we learn how to sort an array of objects with multiple criteria.
The final implementation can sum up like this.
A multiple criteria sorting mean a sorting in which we compare the first criteria, and only if the first criteria is equals, we then go to the next one. We do this until we find a non-equals criterion.
Which can translate into pseoducode like this:
let sortedObjects = objects.sorted { (lhs, rhs) in
for (lhsCriteria, rhsCriteria) in [(lhsCrtria1, rhsCriteria1), (lhsCrtria2, rhsCriteria2), (lhsCrtria3, rhsCriteria3), ... , (lhsCrtriaN, rhsCriteriaN)] {
if lhsCriteria == rhsCriteria {
continue
}
return lhsCriteria < rhsCriteria
}
}
It involves declaring an array of predicates and looping, but we can remove most of that logic with tuple.
Tuple comparison
Swift has overload comparison operators like <, >, <=, >= for us with the following implementation.
/// 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)`).
///
/// - Parameters:
/// - lhs: A tuple of `Comparable` elements.
/// - rhs: Another tuple of elements of the same type as `lhs`.
@inlinable public func < <A, B>(lhs: (A, B), rhs: (A, B)) -> Bool where A : Comparable, B : Comparable
I want you to focus on the following sentence:
/// 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)`).
This is exactly how we implement our sorting. Let's compare that to our pseudocode. You can see that tuples comparison is how we compare our sorting criteria.
let sortedObjects = objects.sorted { (lhs, rhs) in
for (lhsCriteria, rhsCriteria) in [(lhsCrtria1, rhsCriteria1), (lhsCrtria2, rhsCriteria2), (lhsCrtria3, rhsCriteria3), ... , (lhsCrtriaN, rhsCriteriaN)] {
if lhsCriteria == rhsCriteria {
continue
}
return lhsCriteria < rhsCriteria
}
}
Let's implement the sorting using a tuple of criteria.
You can easily support sarunw.com by checking out this sponsor.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
Example
We will use the same object as our last example.
struct BlogPost {
let title: String
let pageView: Int
let sessionDuration: Double
}
Ascending order
To sort in ascending order, we use less than operator (<
) over tuples.
let posts: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 2, sessionDuration: 1),
BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]
let sorted = posts.sorted { (lhs, rhs) -> Bool in
return (lhs.title, lhs.pageView, lhs.sessionDuration) < (rhs.title, rhs.pageView, rhs.sessionDuration)
}
print(sorted)
Here is the result:
[BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
BlogPost(title: "Alice", pageView: 2, sessionDuration: 1.0),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2.0),
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1.0)]
This will compare the first element lhs.title < rhs.title
and if lhs.title == rhs.title
, we compare (lhs.pageView, lhs.sessionDuration) < (rhs.pageView, rhs.sessionDuration)
. This is identical to our previous implmentation.
Descending order
To sort in descending order, we use more than operator (>
) over tuples.
let posts: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 2, sessionDuration: 1),
BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]
let sorted = posts.sorted { (lhs, rhs) -> Bool in
return (lhs.title, lhs.pageView, lhs.sessionDuration) > (rhs.title, rhs.pageView, rhs.sessionDuration)
}
print(sorted)
Here is the result:
[BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1.0),
BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2.0),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Alice", pageView: 2, sessionDuration: 1.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0)]
Different sort order on each element
If you want different sorting orders for each property, you can do that by swap the element's position in the tuples.
We can't change the operator for each element in the tuples, but swapping its position would result in the same effect. Consider the following examples.
lhs.property1 < rhs.proerty1
will sort in ascending order.
lhs.property2 > rhs.proerty2
will sort in descending order, but all elements must compare using the same operator. So, we swap the position.
rhs.property2 < lhs.property2
is equals to lhs.property2 > rhs.property2
which is descending sort.
So, if we want to sort title
in ascending order and sort pageView
and sessionDuration
in descending order, we can use the following code.
let posts: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 2, sessionDuration: 1),
BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]
let sorted = posts.sorted { (lhs, rhs) -> Bool in
return (lhs.title, rhs.pageView, rhs.sessionDuration) < (rhs.title, lhs.pageView, lhs.sessionDuration)
}
print(sorted)
You can easily support sarunw.com by checking out this sponsor.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
Caveats
We can only compare up to six properties using tuples. Swift only overload tuple comparison up to six elements.
/// Returns a Boolean value indicating whether the first tuple is ordered
/// after or the same as the second in a lexicographical ordering.
///
/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is after or the same as the second tuple if and only if
/// `a1 > b1` or (`a1 == b1` and
/// `(a2, ..., aN) >= (b2, ..., bN)`).
///
/// - Parameters:
/// - lhs: A tuple of `Comparable` elements.
/// - rhs: Another tuple of elements of the same type as `lhs`.
@inlinable public func >= <A, B, C, D, E, F>(lhs: (A, B, C, D, E, F), rhs: (A, B, C, D, E, F)) -> Bool where A : Comparable, B : Comparable, C : Comparable, D : Comparable, E : Comparable, F : Comparable
I would say six properties should be enough for most cases, but if it isn't, you can go back to my last implementation.
Read more article about Swift, Sort, Array, Tuple, or see all available topic
Enjoy the read?
If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.
Become a patron Buy me a coffee Tweet ShareHow to create segmented control in SwiftUI
Learn the way to create the UISegmentedControl equivalent in SwiftUI.