let now: Date = .now
let myData: [MyData] = [
.init(time: .distantFuture),
.init(time: .distantPast),
.init(time: now)
]
#expect(
myData.sorted(by: \.time).map(\.time) == [
.distantPast, now, .distantFuture
]
)
public extension Sequence {
@inlinable func sorted<each Comparable: Swift.Comparable, Error>(
by comparable: (Element) throws(Error) -> (repeat each Comparable)
) throws(Error) -> [Element] {
do {
return try sorted { try (repeat each comparable($0)) < (repeat each comparable($1)) }
} catch {
throw error as! Error
}
}
}
@inlinable public func < <each Element: Comparable>(
_ element0: (repeat each Element),
_ element1: (repeat each Element)
) -> Bool {
for elements in repeat (each element0, each element1) {
if elements.0 < elements.1 { return true }
if elements.0 > elements.1 { return false }
}
return false
}
(I've tried to figure out how/if you could pass in >, instead of having to reverse the sorted array, but the compiler gets crashy with parameter packs. It's easy if you don't use packs.)
Also keep in mind that KeyPath access might have a performance overhead you don't have to pay with direct member access. I believe KeyPath access on struct types should be pretty fast… but last I remember there were some TODO optimizations missing for KeyPath access on class types.
I use something very similar in my projects moreso for puzzle scenarios like AoC. I made the latter method quickly just now to parallel existing sorted(by:).
Sorting is also O(n log n). Memoization can be an important optimization to have available. KeyPath conforms to Hashable. It is easy to confirm if two KeyPaths have changed over time (and the sorted results should be recomputed). With two closures… there isn't really any stable and easy concept of "value equality" you can leverage to determine if the user changed the sort order.