Skip to content

Instantly share code, notes, and snippets.

@calda
Created July 20, 2018 00:45
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save calda/792255d239863ab9652cdd4ae7e42ab1 to your computer and use it in GitHub Desktop.
Save calda/792255d239863ab9652cdd4ae7e42ab1 to your computer and use it in GitHub Desktop.
SortDescriptor.playground
import Foundation
// MARK: SortDescriptor
/// Type-erased Sort Descriptor (can store multiple in the same array
/// regardless of the underlying KeyPath
public struct SortDescriptor<Element> {
private let comparator: (Any, Any) -> Bool
// Initialize the `SortDescriptor` with just a Comparator
public init(using comparator: (Element, Element) -> Bool) {
self.comparator { anyLhs, anyRhs in
// un-erase the (Any, Any)
guard let lhs = anyLhs as? Element,
let rhs = anyRhs as? Element else
{
return false
}
return comparator(lhs, rhs)
}
}
// For abitrary `Value` types, the consumer has to provide the entire Comparator
public init<Value>(
_ keyPath: KeyPath<Element, Value>,
using comparator: @escaping (Value, Value) -> Bool)
{
self.init(using: { lhs, rhs in
return comparator(lhs[keyPath: keyPath], rhs[keyPath: keyPath])
})
}
// `Comparable` types have all of the pieces we need to build a SortDescriptor straight from a `KeyPath`
public init<Value: Comparable>(_ keyPath: KeyPath<Element, Value>) {
self.init(keyPath, using: <)
}
// This initializer specifically lets us use methods like `String.caseInsensitiveCompare` in an ergonomic way.
// FIXME: Can we add this in an extension that only gets applied if Foundation is importable?
// It may be better to just have a `(Value, Value) -> ComparisonResult` in case something like SE-42 is ever implemented.
public init<Value>(
_ keyPath: KeyPath<Element, Value>,
using curriedComparator: @escaping (Value) -> (Value) -> ComparisonResult)
{
self.init(keyPath, using: { lhs, rhs -> Bool in
return curriedComparator(lhs)(rhs) == .orderedAscending
})
}
/// Compares the provided elements using the provided KeyPath and comparison procedure
public func orders(_ a: Element, before b: Element) -> Bool {
return self.comparator(a, b)
}
/// Creates a new Sort Descriptor that represents the reverse of this Sort Descriptor
public var reversed: SortDescriptor {
return SortDescriptor(using: { lhs, rhs in
if self.comparator(lhs, rhs) {
return false
} else if self.comparator(rhs, lhs) {
return true
} else {
// this is the `orderedSame` / `==` case.
return false
}
})
}
}
// MARK: Collection + SortDescriptor
extension Collection {
public func sorted<Value: Comparable>(by keyPath: KeyPath<Self.Element, Value>) -> [Self.Element] {
return self.sorted(by: SortDescriptor(keyPath))
}
public func sorted(by sortDescriptor: SortDescriptor<Self.Element>) -> [Self.Element] {
return self.sorted(by: [sortDescriptor])
}
public func sorted(by descriptors: [SortDescriptor<Self.Element>]) -> [Self.Element] {
return self.sorted(by: { lhs, rhs in
for descriptor in descriptors {
if descriptor.orders(lhs, before: rhs) {
return true
}
else if descriptor.orders(rhs, before: lhs) {
return false
}
// otherwise, a == b. Fall through to the next descriptor.
}
return false
})
}
}
// MARK: Examples
struct Person {
let name: String
let age: Int
}
let people = [
Person(name: "Alice", age: 34),
Person(name: "Bob", age: 28),
Person(name: "Bob", age: 34),
Person(name: "Eve", age: 30)]
people.sorted(by: \.name)
people.sorted(by: SortDescriptor(\.age).reversed)
people.sorted(by: [
SortDescriptor(\.age).reversed,
SortDescriptor(\.name, using: String.caseInsensitiveCompare)])
@AKoulabukhov
Copy link

AKoulabukhov commented Nov 22, 2018

Amazing!

@heathermeeker
Copy link

Hi, this is great! Thanks for sharing. Could you apply a license like MIT or CC0 so we can use it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment