'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