Last active
March 23, 2020 23:07
-
-
Save AliSoftware/e03cda3c97d4adddce55026571a7145b to your computer and use it in GitHub Desktop.
Inspired by https://forums.swift.org/t/sorting-collections-with-map-closures-and-sortdescriptors/16276
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: ## Demo | |
struct User: CustomStringConvertible { | |
let name: String | |
let age: Int | |
var description: String { "\(name) (\(age))" } | |
} | |
let users = [ | |
User(name: "Bob", age: 22), | |
User(name: "Alice", age: 11), | |
User(name: "Bob", age: 12), | |
User(name: "Camille", age: 18), | |
User(name: "Bob", age: 16), | |
User(name: "Anthony", age: 8), | |
] | |
// Sort by name, ascending | |
users.sorted(by: \.name) | |
// Sort by age, descending | |
users.sorted(by: \.age, .descending) | |
// Sort by name ascending, then by age descending | |
users.sorted(by: .ascending(\.name), .descending(\.age)) | |
// Combine SortDescriptors | |
let sortByName = SortDescriptor.ascending(\User.name) | |
let oldestFirst = SortDescriptor.descending(\User.age) | |
let mySort = sortByName.then(oldestFirst) | |
let mySort2 = SortDescriptor([sortByName, oldestFirst]) // strictly equivalent to the above | |
let mySort3 = SortDescriptor<User>(.ascending(\.name), .descending(\.age)) // likewise | |
users.sorted(by: mySort) | |
mySort.apply(to: users) // strictly equivalent to the above | |
// Thanks to callAsFunction / SE-0253 | |
#if swift(>=5.2) | |
sortByName(users) | |
mySort(users) | |
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: SortDescriptor | |
struct SortDescriptor<Element> { | |
let compare: (Element, Element) -> Direction? | |
enum Direction { | |
case ascending | |
case descending | |
func inverted() -> SortDescriptor.Direction { | |
self == .ascending ? .descending : .ascending | |
} | |
} | |
} | |
extension SortDescriptor { | |
init<T: Comparable>(_ lens: @escaping (Element) -> T, direction: Direction = .ascending) { | |
self.compare = { lhs, rhs in | |
let (ll,lr) = (lens(lhs), lens(rhs)) | |
if ll < lr { return direction } | |
if ll > lr { return direction.inverted() } | |
return nil | |
} | |
} | |
// Note: We also get that for free in Swift 5.2 thanks to SE-249 | |
// (though explicit declaration allows to also use non-literal KeyPaths) | |
init<T: Comparable>(_ keyPath: KeyPath<Element,T>, direction: Direction = .ascending) { | |
self.init({ $0[keyPath: keyPath] }, direction: direction) | |
} | |
} | |
// Convenience constructors | |
extension SortDescriptor { | |
static func ascending<T: Comparable>(_ keyPath: KeyPath<Element,T>) -> SortDescriptor<Element> { | |
SortDescriptor(keyPath, direction: .ascending) | |
} | |
static func descending<T: Comparable>(_ keyPath: KeyPath<Element,T>) -> SortDescriptor<Element> { | |
SortDescriptor(keyPath, direction: .descending) | |
} | |
} | |
// Combine SortDescriptors | |
extension SortDescriptor { | |
init<C: Collection>(_ descriptors: C) where C.Element == SortDescriptor { | |
self.compare = { lhs, rhs in | |
for desc in descriptors { | |
if let res = desc.compare(lhs, rhs) { return res } | |
} | |
return nil | |
} | |
} | |
init(_ descriptors: SortDescriptor...) { | |
self.init(descriptors) // transform varargs to array and call above function | |
} | |
func then(_ other: SortDescriptor) -> SortDescriptor { | |
SortDescriptor([self, other]) | |
} | |
} | |
// apply(to:) / SE-0253 callAsFunction support | |
extension SortDescriptor { | |
func apply<C: Collection>(to list: C) -> [Element] where C.Element == Element { | |
return list.sorted(by: self) | |
} | |
#if swift(>=5.2) | |
// Support for SE-0253 in Swift 5.2 | |
func callAsFunction<C: Collection>(_ list: C) -> [Element] where C.Element == Element { | |
return self.apply(to: list) | |
} | |
#endif | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: Sorting collections | |
extension Collection { | |
typealias SortDirection = SortDescriptor<Element>.Direction | |
// MARK: Convenience methods | |
func sorted<T: Comparable>(by closure: (Element) -> T, _ direction: SortDirection = .ascending) -> [Element] { | |
withoutActuallyEscaping(closure) { | |
sorted(by: SortDescriptor($0, direction: direction)) | |
} | |
} | |
// Note: We also get that for free in Swift 5.2 thanks to SE-249 | |
// (though explicit declaration allows to also use non-literal KeyPaths) | |
func sorted<T: Comparable>(by keyPath: KeyPath<Element,T>, _ direction: SortDirection = .ascending) -> [Element] { | |
sorted(by: SortDescriptor(keyPath, direction: direction)) | |
} | |
func sorted(by descriptors: SortDescriptor<Element>...) -> [Element] { | |
sorted(by: descriptors) // convert varargs to array and call the one with array | |
} | |
func sorted(by descriptors: [SortDescriptor<Element>]) -> [Element] { | |
return sorted(by: SortDescriptor(descriptors)) | |
} | |
// MARK: Main logic | |
func sorted(by descriptor: SortDescriptor<Element>) -> [Element] { | |
return sorted(by: { (lhs: Element, rhs: Element) -> Bool in | |
descriptor.compare(lhs, rhs) == .ascending | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment