swift sort array by two properties [duplicate] - arrays

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?

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)

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.

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.

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
)

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.

How about:
contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }

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}

Related

Why am I being told city had no member .sort

How do I fix this? I've tried different things
struct city {
let name: String
let county: String
let State: String
let Population: Int
}
let city1 = city(name: "Knoxville", county: "Knox", State: "Tennessee", Population: 186173)
let city2 = city(name: "Clarksville", county: "Montgomery", State: "Tennessee", Population: 152934)
let city3 = city(name: "Nashville", county: "Davidson", State: "Tennessee", Population: 692587)
let city4 = city(name: "Chattanooga", county: "Hamilton", State: "Tennessee", Population: 179690)
let city5 = city(name: "Memphis", county: "Shelby", State: "Tennessee", Population: 651932)
var CityArray = (city1, city2, city3, city4, city5)
func CityLists(List: [city]) {
city.sort{ $0.Population < $1.Population }
}
There are a few issues here.
You should conform to the Swift convention of capitalizing type names and using lowercase names for variables and properties. That will make this less confusing.
Your CityArray doesn't use array syntax, which uses square brackets
You don't use the List parameter you send in to your CityLists function.
It's unclear whether you want to sort in-place or return an array -- I've chosen the latter in my example:
struct City {
var name: String
var county: String
var state: String
var population: Int
}
let city1 = City(name: "Knoxville", county: "Knox", state: "Tennessee", population: 186173)
let city2 = City(name: "Clarksville", county: "Montgomery", state: "Tennessee", population: 152934)
let city3 = City(name: "Nashville", county: "Davidson", state: "Tennessee", population: 692587)
let city4 = City(name: "Chattanooga", county: "Hamilton", state: "Tennessee", population: 179690)
let city5 = City(name: "Memphis", county: "Shelby", state: "Tennessee", population: 651932)
var cityArray = [city1, city2, city3, city4, city5]
func cityLists(list: [City]) -> [City] {
list.sorted { $0.population < $1.population }
}
print(cityLists(list: cityArray))
If for some reason you wanted to sort in place (not recommended, as it's not very Swift-y). you could do:
func cityLists(list: inout [City]) {
list.sort { $0.population < $1.population }
}
cityLists(list: &cityArray)
You asked:
Why am I being told city had no member .sort
This is because:
the CityArray is not an array, but rather a tuple (because you used parentheses rather than square brackets);
the CityLists is not using the parameter you passed to the method; and
even if you did change CityLists to use its parameter, the sort method (which does a “sort in place”, sorting the existing array rather than returning a new sorted array) only works with a mutable collections; but your sorting method has a list parameter, which is an immutable copy of your array.
So, consider the following (using standard name capitalization conventions):
struct City {
let name: String
let county: String
let state: String
let population: Int
}
let city1 = City(name: "Knoxville", county: "Knox", state: "Tennessee", population: 186173)
let city2 = City(name: "Clarksville", county: "Montgomery", state: "Tennessee", population: 152934)
let city3 = City(name: "Nashville", county: "Davidson", state: "Tennessee", population: 692587)
let city4 = City(name: "Chattanooga", county: "Hamilton", state: "Tennessee", population: 179690)
let city5 = City(name: "Memphis", county: "Shelby", state: "Tennessee", population: 651932)
var cities = [city1, city2, city3, city4, city5] // note the square brackets, which make this an array, not parentheses, which would make it a tuple
You can either “sort in place” by making the parameter an inout, making the parameter mutable:
func sortByPopulation(_ array: inout [City]) {
array.sort{ $0.population < $1.population }
}
sortByPopulation(&cities)
Or you can write a function to return a new sorted array, leaving the original one “as is”:
func sortedByPopulation(_ array: [City]) -> [City] {
return array.sorted { $0.population < $1.population }
}
let sortedCities = sortedByPopulation(cities)
A slightly more advanced idea might be to add population sorting routines to arrays of cities:
extension Array where Element == City {
mutating func sortByPopulation() {
sort { $0.population < $1.population }
}
func sortedByPopulation() -> [City] {
return sorted { $0.population < $1.population }
}
}
Then you can do things like:
cities.sortByPopulation()
Or:
let sortedCities = cities.sortedByPopulation()
This leads to more natural reading code.
And, before someone complains about using an Array extension (which I did to keep it relatively simple), one probably would define these methods on broader purpose protocols, rather than only Array instances. E.g.:
extension MutableCollection where Self: RandomAccessCollection, Element == City {
mutating func sortByPopulation() {
sort { $0.population < $1.population }
}
}
extension Sequence where Element == City {
func sortedByPopulation() -> [City] {
return sorted { $0.population < $1.population }
}
}
This is a slightly more flexible pattern, permitting sorting of any sequence or mutable collection of cities, not just arrays of them.
If you’ve followed me this far down the rabbit hole, you might want to see Swift by Sundell’s Sorting Swift collections for other patterns.
E.g., I particularly like the keypath generic pattern:
extension MutableCollection where Self: RandomAccessCollection {
mutating func sort<T: Comparable>(by keyPath: KeyPath<Element, T>) {
sort { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
}
}
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
}
}
Then you can do:
cities.sort(by: \.population)
let sorted = cities.sorted(by: \.population)

Reduce an array of objects based on object field

I have a Country object and a City object
struct Country: {
let name: String
let countryCode: String
let cities: [City]
let population: Int
init(name: String, countryCode: String, cities: [City], population: Int) {
self.name = name
self.countryCode = countryCode
self.cities = cities
self.population = population
}
}
struct City {
let id: Int
let name: String
let latitude: Double
let longitude: Double
let countryCode: String
let population: Int
}
Incoming JSON data looks like this which decodes into [City] array
{
"cities":[
{
"id":1,
"name":"Paris",
"latitude":0,
"logitude":0,
"country_code":"FR",
"population":0
},
{
"id":2,
"name":"Nice",
"latitude":0,
"logitude":0,
"country_code":"FR",
"population":0
},
{
"id":3,
"name":"Berlin",
"latitude":0,
"logitude":0,
"country_code":"DE",
"population":0
},
{
"id":4,
"name":"Munich",
"latitude":0,
"logitude":0,
"country_code":"DE",
"population":0
},
{
"id":5,
"name":"Amsterdam",
"latitude":0,
"logitude":0,
"country_code":"NL",
"population":0
},
{
"id":6,
"name":"Leiden",
"latitude":0,
"logitude":0,
"country_code":"NL",
"population":0
}
]
}
How would I create [Country] array from [City] array efficiently? I've tried to use reduce:into: but not sure that's what I have to use.
I know I could go with an empty array and add/create Countries one by one then search if there is one already and add City to it. That creates awful looking code as for me. I feel like there is an elegant solution to this problem using map or reduce functions.
reduce:into: code I've tried so far
func transformArrayOf(_ cities: [City]) -> [Country] {
let empty: [Country] = []
return cities.reduce(into: empty) { countries, city in
let existing = countries.filter { $0.countryCode == city.countryCode }.first
countries[existing].cities.append(city)
}
}
EDIT:
The function only gets [City] array. So countries must be created only from that.
Dictionary(grouping:by:) with map(_:) works perfectly! Two lines instead on nested for loops and if statements :)
And Country name can be parsed from a country code
Use Dictionary(grouping:by:) and map(_:) combined to get the expected result.
let countries = Dictionary(grouping: cities, by: { $0.countryCode }).map { (countryCode, cities) -> Country in
return Country(name: "", countryCode: countryCode, countryName: "", cities: cities, population: cities.reduce(0) { $0 + $1.population })
}
Since the values for name and countryName are unknown, I've used empty String ("") for both.
This is what Dictionary(grouping:by:) is for:
let citiesByCountryCode = Dictionary(grouping: cities, by: \.countryCode)
But you'll need separate logic to create the countries, because they contain data that isn't derived from the cities like name, countryName (how are those different?), etc.

Sort an Array by two values Swift 4 [duplicate]

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?
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)
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.
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.
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
)
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.
How about:
contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
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}

Swift - Grouping elements according to their equal features

In Swift 4, I have:
let customers = [Customer(name: "John", country: "US", profession: "Engineer"), Customer(name: "Mary", country: "UK", profession: "Nurse"), Customer(name: "Diana", country: "US", profession: "Engineer"), Customer(name: "Paul", country: "US", profession: "Plumber"), Customer(name: "Sam", country: "UK", profession: "Nurse")]
I would like to have for example a function that could filter the elements in customers, so that each time the names and professions of at least 2 elements in it are equal, they are added to an array automatically created by this function :
var customers1 = [Customer(name: "John", country: "US", profession: "Engineer"), Customer(name: "Diana", country: "US", profession: "Engineer")]
var customers2 = [Customer(name: "Mary", country: "UK", profession: "Nurse"), Customer(name: "Sam", country: "UK", profession: "Nurse")]
I searched without success, however I picked some solutions that perhaps could be adapted to this case:
extension Array where Element: Comparable {
func containsSameElements(as other: [Element]) -> Bool {
return self[1] == other[1] && self[2] == other[2]
}
}
or
func ==<Element : Equatable> (lhs: [[Element]], rhs: [[Element]]) -> Bool {
return lhs.elementsEqual(rhs, by: ==)
}
or
elementsEqual()/contains() with a loop.
or
a combination of flatMap()/reduce()/filter().
Thank you.
Based on your feedback and clarification, I would do something like this.
struct CountryAndProfession: Hashable, CustomStringConvertible {
let country: String
let profession: String
var description: String {
return "CountryAndProfession{country: \(country), profession: \(profession)}"
}
var hashValue: Int {
return "\(country)__\(profession)".hashValue
}
static func ==(left: CountryAndProfession, right: CountryAndProfession) -> Bool {
return left.country == right.country && left.profession == right.profession
}
}
// The Customer Type you apparently are dealing with. (May need a custom init depending on your use case
struct Customer: CustomStringConvertible {
let name: String
let countryAndProfession: CountryAndProfession
var description: String {
return "Customer{name: \(name), countryAndProfession: \(countryAndProfession)}"
}
// returns a dictionary with the passed customer's CountryAndProfession as the key, and the matching
static func makeDictionaryWithCountryAndProfession(from customers: [Customer]) -> [CountryAndProfession : [Customer]] {
var customersArrayDictionary: [CountryAndProfession : [Customer]] = [:]
customers.forEach { (customer) in
if customersArrayDictionary.keys.contains(customer.countryAndProfession) {
customersArrayDictionary[customer.countryAndProfession]?.append(customer)
}
else {
customersArrayDictionary[customer.countryAndProfession] = [customer]
}
}
return customersArrayDictionary
}
static func getArraysBasedOnCountries(from customerArray: [Customer]) -> [[Customer]] {
return Array(makeDictionaryWithCountryAndProfession(from: customerArray).values)
}
}
let arrayOfArrays = [["John", "A", "A" ], ["Mary", "A", "B" ], ["Diana", "A", "A" ], ["Paul", "B", "B" ], ["Sam", "A", "B" ]]
//If you're dealing with non-predictable data, you should probably have some Optionality
let allCustomers = arrayOfArrays.map{ Customer(name: $0[0], countryAndProfession: CountryAndProfession(country: $0[1], profession: $0[2])) }
let splitCustomers = Customer.getArraysBasedOnCountries(from: allCustomers)
//contains [[John, Diana], [Mary, Sam], [Paul]]
I'm still not quite sure what you want your final result to look like (something that is always helpful to put in the question), but you should be able to get the result you're looking for using makeDictionaryWithCountryAndProfession combined with the specific CountryAndProfession you're looking for or using .filter
This is how I would recommend doing what you're trying to do:
struct Customer {
let name: String
let country: String
let profession: String
func countryMatches(with otherCustomer: Customer) -> Bool {
return country == otherCustomer.country
}
func professionMatches(with otherCustomer: Customer) -> Bool {
return profession == otherCustomer.profession
}
func countryAndProfessionMatch(with otherCustomer: Customer) -> Bool {
return countryMatches(with: otherCustomer) && professionMatches(with: otherCustomer)
}
static func getAllCustomersWithProfessionsAndCountriesMatching(with customer: Customer, from allCustomers: [Customer]) -> [Customer] {
return allCustomers.filter { customer.countryAndProfessionMatch(with: $0) }
}
}
let arrayOfArrays = [["John", "A", "A" ], ["Mary", "A", "B" ], ["Diana", "A", "A" ], ["Paul", "B", "B" ], ["Sam", "A", "B" ]]
//If you're dealing with non-predictable data, you should probably have some Optionality
let allCustomers = arrayOfArrays.map{ Customer(name: $0[0], country: $0[1], profession: $0[2]) }
let allCustomersMatchingJohnProperties = Customer.getAllCustomersWithProfessionsAndCountriesMatching(with: allCustomers[0], from: allCustomers)
// contains John and Diane
let allCustomersMatchingMaryProperties = Customer.getAllCustomersWithProfessionsAndCountriesMatching(with: allCustomers[1], from: allCustomers)
// contains Mary and Sam
I believe this does what you're looking to do, but with a more structured/maintainable approach.
getAllCustomersWithProfessionsAndCountriesMatching is almost definitely way too long, but left it that way to be clear for the answer. I would advice renaming it to fit your use case.

Finding value in array containing custom class

I'm trying to find the place of a value in a an array containing structures.
My array looks like this
struct User {
var firstName: String?
var lastName: String?
}
var allThePeople = [User(firstName: "John", lastName: "Doe"), User(firstName: "Jane", lastName: "Doe"), User(firstName: "John", lastName: "Travolta")];
Is there a way to get the places for all "Doe"'s in the array? (in this case 0 and 1)
You can filter allThePeople with a condition to get all the people with the last name "Doe".
let allTheDoes = allThePeople.filter { $0.lastName == "Doe" }
You can enumerate the array and flat map it to an array of indices.
let allTheDoeIndexes = allThePeople.enumerated().flatMap { $0.element.lastName == "Doe" ? $0.offset : nil }
= allThePeople.enumerated().flatMap { $1.lastName == "Doe" ? $0 : nil }
If you want the actual indices, use something like
struct User {
var firstName: String?
var lastName: String?
}
var allThePeople = [User(firstName: "John", lastName: "Doe"), User(firstName: "Jane", lastName: "Doe"), User(firstName: "John", lastName: "Travolta")]
var indices = [Int]()
for i in 0 ..< allThePeople.count {
if allThePeople[i].lastName == "Doe" {
indices.append(i)
}
}
indices // [0,1]
otherwise use filter as #Callam suggested.

Resources