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?
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]
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]
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 ?? "~" }