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?
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.
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 useSequence.sorted()
method, which consults your implementation of theComparable.<(_:_:)
operator to determine how elements compare. This way, you can sort anySequence
ofContact
s without ever having to duplicate the sorting code.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:
How about:
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 bylastName
, iflastName
's are equal, then sort by the second field, in this casefirstName
.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:Here are the functions that allow you to do this:
If you want to test it out, you can use this extra code:
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 ofAnimal.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 insort
/sorted
method but requires two additional ones to be defined:compare
andcomparisons
.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, assumingprofileViews
is an expensive network call we may want to avoid callingprofileViews
unless it's absolutely necessary. My solution will avoid evaluatingprofileViews
until$0.name == $1.name
and$0.address == $1.address
. However, when it does evaluateprofileViews
it'll likely evaluate many more times than once.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.For example:
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 thefirstName
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:
This will now sort by
lastName
descending, thenfirstName
ascending.Defining a
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 ofsort(by:)
andsorted(by:)
that deals with multiple predicates – where each predicate is considered in turn to decide the order of the elements.(The
secondPredicate:
parameter is unfortunate, but is required in order to avoid creating ambiguities with the existingsort(by:)
overload)This then allows us to say (using the
contacts
array from earlier):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
toComparable
:And now just call
sort()
for an ascending order:or
sort(by: >)
for a descending order: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:
and then simply call as: