Created
July 14, 2022 14:45
-
-
Save danielgarbien/d2a2114ed66f6992fb4f3882c7d5e182 to your computer and use it in GitHub Desktop.
Multi-attribute sorting with sort descriptors
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
import Foundation | |
struct SortDescriptor<Value> { | |
let compare: (Value, Value) -> ComparisonResult | |
} | |
extension Collection { | |
func sorted(by sortDescriptors: [SortDescriptor<Element>]) -> [Element] { | |
sorted { a, b in | |
for sortDescriptor in sortDescriptors { | |
switch sortDescriptor.compare(a, b) { | |
case .orderedAscending: | |
return true | |
case .orderedDescending: | |
return false | |
case .orderedSame: | |
break | |
} | |
} | |
return false | |
} | |
} | |
} | |
extension SortDescriptor { | |
static func attribute<Attribute>(_ attribute: @escaping (Value) -> Attribute, sortDescriptor: SortDescriptor<Attribute>) -> Self { | |
self.init { a, b in | |
sortDescriptor.compare(attribute(a), attribute(b)) | |
} | |
} | |
func reversed() -> Self { | |
SortDescriptor { a, b in | |
compare(a, b).opposite | |
} | |
} | |
func handlingOptionals(nilValuesAtTheEnd: Bool) -> SortDescriptor<Value?> { | |
SortDescriptor<Value?> { a, b in | |
switch (a, b) { | |
case (.none, .none): | |
return .orderedSame | |
case (.none, .some): | |
return nilValuesAtTheEnd ? .orderedDescending : .orderedAscending | |
case (.some, .none): | |
return nilValuesAtTheEnd ? .orderedAscending : .orderedDescending | |
case let (.some(a), .some(b)): | |
return compare(a, b) | |
} | |
} | |
} | |
} | |
extension SortDescriptor where Value: Comparable { | |
init(ascending: Bool) { | |
self.init { a, b in | |
ascending | |
? a.compare(b) | |
: a.compare(b).opposite | |
} | |
} | |
} | |
extension SortDescriptor { | |
static func comparableAttribute<ComparableAttribute: Comparable>( | |
_ comparableAttribute: @escaping (Value) -> ComparableAttribute, | |
ascending: Bool | |
) -> Self { | |
.attribute( | |
comparableAttribute, | |
sortDescriptor: .init(ascending: ascending) | |
) | |
} | |
static func comparableAttribute<ComparableAttribute: Comparable>( | |
_ comparableAttribute: @escaping (Value) -> ComparableAttribute?, | |
ascending: Bool, | |
nilValuesAtTheEnd: Bool | |
) -> Self { | |
.attribute( | |
comparableAttribute, | |
sortDescriptor: SortDescriptor<ComparableAttribute>(ascending: ascending) | |
.handlingOptionals(nilValuesAtTheEnd: nilValuesAtTheEnd) | |
) | |
} | |
static func ascending<ComparableAttribute: Comparable>( | |
_ comparableAttribute: @escaping (Value) -> ComparableAttribute | |
) -> Self { | |
.comparableAttribute(comparableAttribute, ascending: true) | |
} | |
static func descending<ComparableAttribute: Comparable>( | |
_ comparableAttribute: @escaping (Value) -> ComparableAttribute | |
) -> Self { | |
.comparableAttribute(comparableAttribute, ascending: false) | |
} | |
static func ascending<ComparableAttribute: Comparable>( | |
_ comparableAttribute: @escaping (Value) -> ComparableAttribute?, | |
nilValuesAtTheEnd: Bool | |
) -> Self { | |
.comparableAttribute(comparableAttribute, ascending: true, nilValuesAtTheEnd: nilValuesAtTheEnd) | |
} | |
static func descending<ComparableAttribute: Comparable>( | |
_ comparableAttribute: @escaping (Value) -> ComparableAttribute?, | |
nilValuesAtTheEnd: Bool | |
) -> Self { | |
.comparableAttribute(comparableAttribute, ascending: false, nilValuesAtTheEnd: nilValuesAtTheEnd) | |
} | |
} | |
extension Comparable { | |
func compare(_ other: Self) -> ComparisonResult { | |
if self < other { | |
return .orderedAscending | |
} | |
if self > other { | |
return .orderedDescending | |
} | |
return .orderedSame | |
} | |
} | |
extension ComparisonResult { | |
var opposite: ComparisonResult { | |
switch self { | |
case .orderedSame: | |
return .orderedSame | |
case .orderedAscending: | |
return .orderedDescending | |
case .orderedDescending: | |
return .orderedAscending | |
} | |
} | |
} | |
// EXAMPLE | |
struct Person: Hashable, CustomDebugStringConvertible { | |
let firstName: String | |
let lastName: String | |
let rank: Int? | |
var debugDescription: String { | |
let rankString = rank.flatMap { "\($0)" } ?? "–" | |
return "(\(rankString)) \(lastName), \(firstName)" | |
} | |
} | |
let broadus = Person(firstName: "Preston", lastName: "Broadus", rank: 3) | |
let favoritePeople: Set<Person> = [broadus] | |
let people: [Person] = [ | |
broadus, | |
.init(firstName: "D'Angelo", lastName: "Barksdale", rank: 5), | |
.init(firstName: "Avon", lastName: "Barksdale", rank: 9), | |
.init(firstName: "Russel", lastName: "Bell", rank: 9), | |
.init(firstName: "Dennis", lastName: "Wise", rank: nil), | |
] | |
extension SortDescriptor where Value == Bool { | |
static func trueBeforeFalse() -> Self { | |
comparableAttribute({ $0 ? 0 : 1 }, ascending: true) | |
} | |
} | |
let sorted = people.sorted(by: [ | |
.attribute(favoritePeople.contains, sortDescriptor: .trueBeforeFalse()), | |
.descending(\.rank, nilValuesAtTheEnd: true), | |
.ascending(\.lastName), | |
.ascending(\.firstName) | |
]) | |
printInRows(sorted) | |
func printInRows(_ array: [Any]) { | |
array.forEach { print($0) } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment