Skip to content

Instantly share code, notes, and snippets.

@danielgarbien
Created July 14, 2022 14:45
Show Gist options
  • Save danielgarbien/d2a2114ed66f6992fb4f3882c7d5e182 to your computer and use it in GitHub Desktop.
Save danielgarbien/d2a2114ed66f6992fb4f3882c7d5e182 to your computer and use it in GitHub Desktop.
Multi-attribute sorting with sort descriptors
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