seq.sorted(_:)

sort a sequence on multiple properties

有了下面的擴充後,我們就可以用這麼漂亮的語法來對物件排序:

let sorted = items.sorted(
    .descending(\.price), 
    .descending(\.rate), 
    .ascending (\.title)
)

善用 guard ... else 可以讓「用多個屬性來排序」的語法變得更簡潔。

👉 replit

// 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
        }
    }
}

Last updated

Was this helpful?