seq.sorted(_:)
sort a sequence on multiple properties
Last updated
Was this helpful?
sort a sequence on multiple properties
Last updated
Was this helpful?
有了下面的擴充後,我們就可以用這麼漂亮的語法來對物件排序:
let sorted = items.sorted(
.descending(\.price),
.descending(\.rate),
.ascending (\.title)
)
善用 guard ... else 可以讓「用多個屬性來排序」的語法變得更簡潔。
// 1.0.0: 2021.12.11
/*
設計理念:
--------
我們將要用於排序的屬性 key path (`prop`) 傳入 `SortOrder`
的 `isBefore` 與 `isEqual` 兩個 closure 中,如此就可以
直接用 order.isBefore(a, b) 或 order.isEqual(a, b) 來
比較兩個 Element 的大小,而不需要為了要將這個 key path 存起來
而傷腦筋到底要用: KeyPath<Element, Property> 還是要用
PartialKeyPath<Element> 型別?
事實上,用這兩種型別存起來都有問題。
首先,如果用 KeyPath<Element, Property>,這時勢必要將 SortOrder
的型別也改為 SortOrder<Element, Property>,但這樣一改,後面
sorted() 函數的 variadic 參數 orders 也要跟著改,這會造成
所有的屬性都是同一種 Property 型別,直接讓 sorted() 函數無法使用。
再來,如果用 PartialKeyPath<Element> 來存 key path,這時
這個 key path 會失去 (type-erase) 它的 Value type,間接
造成系統無法辨別 element[keyPath: keyPath] 的型別,因此也
不知道原來這個值是一個 Comparable,所以在程式中也沒辦法用 "==",
"<", ">" 這些運算符號,因此就算將這個 key path 存起來也沒什麼用。
*/
/// SortOrder
public struct SortOrder<Element> {
public let isBefore: (Element, Element) -> Bool
public let isEqual : (Element, Element) -> Bool
}
// ⭐️ SortOrder
extension SortOrder {
/// .ascending(\.property)
public static func ascending<Property: Comparable>(
_ prop: KeyPath<Element, Property>
) -> SortOrder<Element>
{
return SortOrder(
isBefore: { $0[keyPath: prop] < $1[keyPath: prop] },
isEqual : { $0[keyPath: prop] == $1[keyPath: prop] }
)
}
/// .descending(\.property)
public static func descending<Property: Comparable>(
_ prop: KeyPath<Element, Property>
) -> SortOrder<Element>
{
return SortOrder(
isBefore: { $0[keyPath: prop] > $1[keyPath: prop] },
isEqual : { $0[keyPath: prop] == $1[keyPath: prop] }
)
}
}
// seq.sorted(_:)
extension Sequence {
/// ⭐️ items in a `Sequence` sorted by multiple properties.
/// example:
/// ```
/// items.sorted(.ascending(\.title))
/// items.sorted(.ascending(\.price), .descending(\.rate))
/// ```
public func sorted(_ orders: SortOrder<Element>...) -> [Element] {
return sorted { a, b in
// ⭐️ if some property is not equal, return the order
for order in orders {
guard order.isEqual(a, b)
else { return order.isBefore(a, b) }
}
// ⭐️ all properties are equal, so `a` is NOT before `b`
return false
}
}
}
// Using extension
struct Post {
let title: String
let views: Int
let duration: Int
}
var posts = [
Post(title: "Alice", views: 1, duration: 3),
Post(title: "Beth" , views: 1, duration: 1),
Post(title: "Cloe" , views: 1, duration: 2),
Post(title: "David", views: 5, duration: 2),
Post(title: "Ethan", views: 4, duration: 10),
Post(title: "Felix", views: 5, duration: 2)
]
// ⭐️ seq.sorted(_:)
let sorted = posts.sorted(
.descending(\.views),
.descending(\.duration),
.ascending (\.title)
)
// [
// Post(title: "David", views: 5, duration: 2),
// Post(title: "Felix", views: 5, duration: 2),
// Post(title: "Ethan", views: 4, duration: 10),
// Post(title: "Alice", views: 1, duration: 3),
// Post(title: "Beth" , views: 1, duration: 2),
// Post(title: "Cloe" , views: 1, duration: 1)
// ]
// Without using extension
struct Post {
let title: String
let views: Int
let duration: Int
}
var posts = [
Post(title: "Alice", views: 1, duration: 3),
Post(title: "Beth", views: 1, duration: 2),
Post(title: "Cloe", views: 1, duration: 1),
Post(title: "David", views: 5, duration: 2),
Post(title: "Ethan", views: 4, duration: 10),
Post(title: "Felix", views: 5, duration: 2)
]
// ------------ main ------------
// sort in place
posts.sort { // $0 is before $1 ?
// check on `views` first
guard $0.views == $1.views else {
return $0.views > $1.views // decreasing
}
// $0.views == $1.views
// check on `duration` next
guard $0.duration == $1.duration else {
return $0.duration > $1.duration // decreasing
}
// $0.views == $1.views
// $0.duration == $1.duration
// check on `title` last
return $0.title < $1.title // increasing
}
print(posts)
// [
// Post(title: "David", views: 5, duration: 2),
// Post(title: "Felix", views: 5, duration: 2),
// Post(title: "Ethan", views: 4, duration: 10),
// Post(title: "Alice", views: 1, duration: 3),
// Post(title: "Beth" , views: 1, duration: 2),
// Post(title: "Cloe" , views: 1, duration: 1)
// ]
Sarunw ⟩
contacts.sorted(by: .ascending(\.lastName), .descending(\.age))
Sorting Swift Collections ⟩ ⭐️
Web Dev N⟩otes ⟩
KeyPathKit ⟩ , ⭐️⭐️⭐️