'Dynamically passing closure with keypaths to a sorting function
I have this method that I use for sorting based on object properties:
extension Sequence {
mutating func sorted(
by predicates: [(Element, Element) -> Bool]
) -> [Element] {
return sorted(by:) { lhs, rhs in
for predicate in predicates {
if predicate(lhs, rhs) { return true }
if predicate(rhs, lhs) { return false }
}
return false
}
}
}
And I can use it like this on myArray of a MyClass type:
myArray.sorted(by: [{$0.propertyA > $1.propertyA}, {$0.propertyB > $1.propertyB}, {$0.propertyC < $1.propertyC}])
but I would like to build these predicates dynamically, so that properties used for sorting are not predefined.
I guess I should be using keyPaths (to store something like KeyPath<MyModel, MyComparableType>, but I had no luck with that.
How would I pass correct operator (less than, bigger than) along with property I want to use for sorting?
Solution 1:[1]
You can simply pass a predicate to return a comparable property from the element of a sequence and another one to check if both elements are in increasing other:
extension Sequence {
public func sorted<T: Comparable>(
_ predicate: (Element) throws -> T, by areInIncreasingOrder: (T, T) throws -> Bool
) rethrows -> [Element] {
try sorted { try areInIncreasingOrder(predicate($0), predicate($1)) }
}
func sorted<T: Comparable>(_ predicate: (Element) throws -> T) rethrows -> [Element] {
try sorted(predicate, by: <)
}
}
extension MutableCollection where Self: RandomAccessCollection {
public mutating func sort<T: Comparable>(
_ predicate: (Element) throws -> T, by areInIncreasingOrder: (T, T) throws -> Bool
) rethrows {
try sort { try areInIncreasingOrder(predicate($0), predicate($1)) }
}
public mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T) rethrows {
try sort(predicate, by: <)
}
}
To suport multiple criteria (secondary, tertiary, and quaternary) you just need to add more generic types to your method:
extension Sequence {
public func sorted<T: Comparable, U: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool)
) rethrows -> [Element] {
try sorted {
let lhs = try primary.0($0)
let rhs = try primary.0($1)
guard lhs == rhs else {
return try primary.1(lhs, rhs)
}
return try secondary.1(secondary.0($0), secondary.0($1))
}
}
public func sorted<T: Comparable, U: Comparable, V: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool),
_ terciary: ((Element) throws -> V, (V, V) throws -> Bool)
) rethrows -> [Element] {
try sorted {
let lhs1 = try primary.0($0)
let rhs1 = try primary.0($1)
guard lhs1 == rhs1 else {
return try primary.1(lhs1, rhs1)
}
let lhs2 = try secondary.0($0)
let rhs2 = try secondary.0($1)
guard lhs2 == rhs2 else {
return try secondary.1(lhs2, rhs2)
}
return try terciary.1(terciary.0($0), terciary.0($1))
}
}
public func sorted<T: Comparable, U: Comparable, V: Comparable, W: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool),
_ terciary: ((Element) throws -> V, (V, V) throws -> Bool),
_ quaternary: ((Element) throws -> W, (W, W) throws -> Bool)
) rethrows -> [Element] {
try sorted {
let lhs1 = try primary.0($0)
let rhs1 = try primary.0($1)
guard lhs1 == rhs1 else {
return try primary.1(lhs1, rhs1)
}
let lhs2 = try secondary.0($0)
let rhs2 = try secondary.0($1)
guard lhs2 == rhs2 else {
return try secondary.1(lhs2, rhs2)
}
let lhs3 = try terciary.0($0)
let rhs3 = try terciary.0($1)
guard lhs3 == rhs3 else {
return try terciary.1(lhs3, rhs3)
}
return try quaternary.1(quaternary.0($0), quaternary.0($1))
}
}
}
Now if you would like to create the mutating version of those methods you need to extend MutableCollection and constrain Selfto RandomAccessCollection:
extension MutableCollection where Self: RandomAccessCollection {
public mutating func sort<T: Comparable, U: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool)
) rethrows {
try sort {
let lhs = try primary.0($0)
let rhs = try primary.0($1)
guard lhs == rhs else {
return try primary.1(lhs, rhs)
}
return try secondary.1(secondary.0($0), secondary.0($1))
}
}
public mutating func sort<T: Comparable, U: Comparable, V: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool),
_ terciary: ((Element) throws -> V, (V, V) throws -> Bool)
) rethrows {
try sort {
let lhs1 = try primary.0($0)
let rhs1 = try primary.0($1)
guard lhs1 == rhs1 else {
return try primary.1(lhs1, rhs1)
}
let lhs2 = try secondary.0($0)
let rhs2 = try secondary.0($1)
guard lhs2 == rhs2 else {
return try secondary.1(lhs2, rhs2)
}
return try terciary.1(terciary.0($0), terciary.0($1))
}
}
public mutating func sort<T: Comparable, U: Comparable, V: Comparable, W: Comparable>(
_ primary: ((Element) throws -> T, (T, T) throws -> Bool),
_ secondary: ((Element) throws -> U, (U, U) throws -> Bool),
_ terciary: ((Element) throws -> V, (V, V) throws -> Bool),
_ quaternary: ((Element) throws -> W, (W, W) throws -> Bool)
) rethrows {
try sort {
let lhs1 = try primary.0($0)
let rhs1 = try primary.0($1)
guard lhs1 == rhs1 else {
return try primary.1(lhs1, rhs1)
}
let lhs2 = try secondary.0($0)
let rhs2 = try secondary.0($1)
guard lhs2 == rhs2 else {
return try secondary.1(lhs2, rhs2)
}
let lhs3 = try terciary.0($0)
let rhs3 = try terciary.0($1)
guard lhs3 == rhs3 else {
return try terciary.1(lhs3, rhs3)
}
return try quaternary.1(quaternary.0($0), quaternary.0($1))
}
}
}
Playground testing:
struct User: Equatable {
let name: String
let age: Int
}
var users: [User] = [
.init(name: "Liza", age: 19),
.init(name: "John", age: 19),
.init(name: "Steve", age: 51)
]
Single property criteria sort:
let sorted = users.sorted(\.age) // [{name "Liza", age 19}, {name "John", age 19}, {name "Steve", age 51}]
users.sort(\.age) // [{name "Liza", age 19}, {name "John", age 19}, {name "Steve", age 51}]
users == sorted // true
Multiple property criteria sort:
let sorted = users.sorted((\.age, >),(\.name, <)) // [{name "Steve", age 51}, {name "John", age 19}, {name "Liza", age 19}]
users.sort((\.age, >),(\.name, <)) // [{name "Steve", age 51}, {name "John", age 19}, {name "Liza", age 19}]
users == sorted // true
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:
Usage:
let sorted1 = users.sorted(using: [KeyPathComparator(\.age)]) // [{name "Liza", age 19}, {name "John", age 19}, {name "Steve", age 51}]
let sorted2 = users.sorted(using: [KeyPathComparator(\.age), KeyPathComparator(\.name)]) // [{name "John", age 19}, {name "Liza", age 19}, {name "Steve", age
let sorted3 = users.sorted(using: [KeyPathComparator(\.age, order: .reverse), KeyPathComparator(\.name)]) // [{name "Steve", age 51}, {name "John", age 19}, {name "Liza", age 19}]
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
