Reputation: 173
I want to create a generic function to sort an array of classes based on a property passed.
For example, I have these classes
public class Car {
var id: Int
var manufacturer: String
var variant: String
init(id: Int, manufacturer: String, variant: String) {
self.id = id
self.manufacturer = manufacturer
self.variant = variant
}
}
enum Gender {
case male
case female
}
public class Person {
var id: Int
var name: String
var age: Int
var gender: Gender
init(id: Int, name: String, age: Int, gender: Gender) {
self.id = id
self.name = name
self.age = age
self.gender = gender
}
}
And these arrays,
let cars = [
Car(id: 1, manufacturer: "Ford", variant: "Focus"),
Car(id: 2, manufacturer: "Nissan", variant: "Skyline"),
Car(id: 3, manufacturer: "Dodge", variant: "Charger"),
Car(id: 4, manufacturer: "Chevrolet", variant: "Camaro"),
Car(id: 5, manufacturer: "Ford", variant: "Shelby")
]
let persons = [
Person(id: 1, name: "Ed Sheeran", age: 26, gender: .male),
Person(id: 2, name: "Phil Collins", age: 66, gender: .male),
Person(id: 3, name: "Shakira", age: 40, gender: .female),
Person(id: 4, name: "Rihanna", age: 25, gender: .female),
Person(id: 5, name: "Bono", age: 57, gender: .male)
]
How to write a generic extension for the array, to sort it based on the property passed? (eg. persons.sort(name) or cars.sort(manufacturer))
Thanks!
Upvotes: 4
Views: 2931
Reputation: 236370
edit/update:
For Xcode 13.0+, iOS 15.0+, iPadOS 15.0+, macOS 12.0+, Mac Catalyst 15.0+, tvOS 15.0+, watchOS 8.0+ you can use KeyPathComparator
:
let sortedPeople1 = people.sorted(using: KeyPathComparator(\.age)) // [{id 4, name "Rihanna", age 25, female}, {id 1, name "Ed Sheeran", age 26, male}, {id 3, name "Shakira", age 40, female}, {id 5, name "Bono", age 57, male}, {id 2, name "Phil Collins", age 66, male}]
let sortedPeople2 = people.sorted(using: KeyPathComparator(\.age, order: .reverse)) // [{id 2, name "Phil Collins", age 66, male}, {id 5, name "Bono", age 57, male}, {id 3, name "Shakira", age 40, female}, {id 1, name "Ed Sheeran", age 26, male}, {id 4, name "Rihanna", age 25, female}]
You can also use multiple sorting criteria and order:
let sortedPeople3 = people.sorted(using: [KeyPathComparator(\.age, order: .reverse), KeyPathComparator(\.name)]) // [{id 2, name "Phil Collins", age 66, male}, {id 5, name "Bono", age 57, male}, {id 3, name "Shakira", age 40, female}, {id 1, name "Ed Sheeran", age 26, male}, {id 4, name "Rihanna", age 25, female}]
let sortedPeople4 = people.sorted(using: [KeyPathComparator(\.age, order: .reverse), KeyPathComparator(\.name)]) // [{id 2, name "Phil Collins", age 66, male}, {id 5, name "Bono", age 57, male}, {id 3, name "Shakira", age 40, female}, {id 1, name "Ed Sheeran", age 26, male}, {id 4, name "Rihanna", age 25, female}]
original answer
Expanding on @MartinR answer and @Sweeper answer to allow increasing (<) or decreasing (>) sort as well as throw and default sort ascending methods:
extension MutableCollection where Self: RandomAccessCollection {
mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T) rethrows {
try sort(predicate, by: <)
}
mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T, by areInIncreasingOrder: ((T, T) throws -> Bool)) rethrows {
try sort { try areInIncreasingOrder(predicate($0), predicate($1)) }
}
}
extension Sequence {
func sorted<T: Comparable>(_ predicate: (Element) throws -> T) rethrows -> [Element] {
try sorted(predicate, by: <)
}
func sorted<T: Comparable>(_ predicate: (Element) throws -> T, by areInIncreasingOrder: ((T,T) throws -> Bool)) rethrows -> [Element] {
try sorted { try areInIncreasingOrder(predicate($0), predicate($1)) }
}
}
people.sorted(\.age)
people.sorted(\.age, by: >)
cars.sorted(\.manufacturer)
cars.sorted(\.manufacturer, by: >)
edit/update:
To suport sorting a custom object by an optional property that conforms to Comparable
protocol:
extension MutableCollection where Self: RandomAccessCollection {
mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T?) rethrows {
try sort(predicate, by: <)
}
mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T?, by areInIncreasingOrder: ((T, T) throws -> Bool)) rethrows {
try sort(by: {
switch try (predicate($0), predicate($1)) {
case let (lhs?, rhs?): return try areInIncreasingOrder(lhs, rhs)
case (.none, _): return false
case (_, .none): return true
}
})
}
}
extension Sequence {
func sorted<T: Comparable>(_ predicate: (Element) throws -> T?) rethrows -> [Element] {
try sorted(predicate, by: <)
}
func sorted<T: Comparable>(_ predicate: (Element) throws -> T?, by areInIncreasingOrder: ((T,T) throws -> Bool)) rethrows -> [Element] {
try sorted(by: {
switch try (predicate($0), predicate($1)) {
case let (lhs?, rhs?): return try areInIncreasingOrder(lhs, rhs)
case (.none, _): return false
case (_, .none): return true
}
})
}
}
Usage:
array.sort(\.optionalStringProperty) {
$0.localizedStandardCompare($1) == .orderedAscending
}
print(array)
Upvotes: 4
Reputation: 271735
Here you go:
extension Array {
mutating func propertySort<T: Comparable>(_ property: (Element) -> T) {
sort(by: { property($0) < property($1) })
}
}
Usage:
persons.propertySort({$0.name})
And here is a non-mutating version:
func propertySorted<T: Comparable>(_ property: (Element) -> T) -> [Element] {
return sorted(by: {property($0) < property($1)})
}
As Leo Dabus pointed out, you can generalise the extension to any MutableCollection
that is also a RandomAccessCollection
:
extension MutableCollection where Self : RandomAccessCollection {
...
Upvotes: 6
Reputation: 539795
Starting with Swift 4 you can define a sorting method which takes a Key-Path Expression as argument. As Leo points out, these methods can be defined more generally as protocols extension methods (for mutable collections and sequences, respectively):
extension MutableCollection where Self: RandomAccessCollection {
// Mutating in-place sort:
mutating func sort<T: Comparable>(byKeyPath keyPath: KeyPath<Element, T>) {
sort(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
}
}
extension Sequence {
// Non-mutating sort, returning a new array:
func sorted<T: Comparable>(byKeyPath keyPath: KeyPath<Element, T>) -> [Element] {
return sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
}
}
Example usage:
persons.sort(byKeyPath: \.name)
cars.sort(byKeyPath: \.manufacturer)
For more information about key-path expressions, see SE-0161 Smart KeyPaths: Better Key-Value Coding for Swift.
Upvotes: 4