Sort nil to the end of an array of optional string

2020-03-29 07:40发布

问题:

If I have an array of optional strings, and I want to sort it in ascending order with nils at the beginning, I can do it easily in a single line:

["b", nil, "a"].sorted{ $0 ?? "" < $1 ?? "" } // [nil, "a", "b"]

But there doesn't seem to be any similarly easy solution to sort nils to the end of the array. It can be easily done with most other simple data types, for instance:

[2, nil, 1].sorted{ $0 ?? Int.max < $1 ?? Int.max } // [1, 2, nil]

For doubles you can do the same with greatestFiniteMagnitude, for dates you can use distantFuture. Is there any kind of equivalent for strings, or any other concise way of doing this so I can avoid writing a bunch of messy conditionals?

回答1:

You can provide a custom comparator which considers nil as larger than any non-nil value:

let array = ["b", nil, "a", nil]

let sortedArray = array.sorted { (lhs, rhs) -> Bool in
    switch (lhs, rhs) {
    case let(l?, r?): return l < r // Both lhs and rhs are not nil
    case (nil, _): return false    // Lhs is nil
    case (_?, nil): return true    // Lhs is not nil, rhs is nil
    }
}

print(sortedArray) // [Optional("a"), Optional("b"), nil, nil]

This works with any array of optional comparable elements, and avoids the usage of “magical large” values. The comparator can be implemented as a generic function:

func compareOptionalsWithLargeNil<T: Comparable>(lhs: T?, rhs: T?) -> Bool {
    switch (lhs, rhs) {
    case let(l?, r?): return l < r // Both lhs and rhs are not nil
    case (nil, _): return false    // Lhs is nil
    case (_?, nil): return true    // Lhs is not nil, rhs is nil
    }
}

print(["b", nil, "a", nil].sorted(by: compareOptionalsWithLargeNil))
// [Optional("a"), Optional("b"), nil, nil]

print([2, nil, 1].sorted(by: compareOptionalsWithLargeNil))
// [Optional(1), Optional(2), nil]

print([3.0, nil, 1.0].sorted(by: compareOptionalsWithLargeNil))
// [Optional(1.0), Optional(3.0), nil]

print([Date(), nil, .distantPast, nil, .distantFuture].sorted(by: compareOptionalsWithLargeNil))
// [Optional(0000-12-30 00:00:00 +0000), Optional(2018-11-22 13:56:03 +0000),
//  Optional(4001-01-01 00:00:00 +0000), nil, nil]


回答2:

One nil is indistinguishable from another. So if you have a working solution that happens to sort as you desire except that nil entries wind up at the start, use it and then remove the nil entries and append the same number of nil entries to the end.

Example:

    var arr : [String?] = [nil, "b", nil, "a", nil]
    arr = arr.sorted{ $0 ?? "" < $1 ?? "" }
    if let ix = arr.firstIndex(where: {$0 != nil}) {
        arr = arr.suffix(from: ix) + Array(repeating: nil, count: ix)
    }
    // [Optional("a"), Optional("b"), nil, nil, nil]


回答3:

Your example with Int gives a clue. If we had a max string value, we could plug that in.

This works for strings that contain only alphabetic characters:

let maxString = "~"
["b", nil, "a"].sorted{ $0 ?? maxString < $1 ?? maxString }

Or simply:

["b", nil, "a"].sorted{ $0 ?? "~" < $1 ?? "~" }