Home > Software design >  Dynamically passing closure with keypaths to a sorting function
Dynamically passing closure with keypaths to a sorting function

Time:01-12

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?

CodePudding user response:

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: <)
    }
}

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>(
        _ 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: <)
    }
    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)
]

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
  •  Tags:  
  • Related