Pete
Pete

Reputation: 219

Sort Through Array Excluding Nil

I am attempting to sort an array which includes variables which are nil.

class TestArray {
    var a: String? = nil
    var b: String? = nil
    init(a: String?, b: String?) {
        self.a = a
        self.b = b
    }
}

let first = TestArray(a: "xxx", b: "yyy")
let second = TestArray(a: "zzz", b: "zzz")
let third = TestArray(a: "aaa", b: nil)

var array = [first, second, third]
array.sort(by: {($0.a as String!) > ($1.a as String!)})

array.sort(by: {($0.b as String!) > ($1.b as String!)}) // Throws an error

How can I sort by b and leave the third TestArray with b = nil at the end of the array? Also I would like to sort through all b and then for the arrays in which b = nil sort the remaining by a.

Upvotes: 0

Views: 530

Answers (3)

Alexander
Alexander

Reputation: 63359

Here's (in my opinion) the best way:

First, I define a < operator for String? instances. It operates for the specification that nil compares as less than any other string, so it'll appear first in sorting order.

fileprivate func <(a: String?, b: String?) -> Bool {
    switch (a, b) {
        case (nil, nil): return false
        case (nil, _?): return true
        case (_?, nil): return false
        case (let a?, let b?): return a < b

    }
}

Next, I wrote a sorting predicate that uses that operator. It works for the specification that instances are to be sorted first by their a. If those are equal, ties are broken by sorting by their b.

struct TestStruct {
    var a: String?
    var b: String?
}

let input = [  
    TestStruct(a: "xxx", b: "yyy"),
    TestStruct(a: "zzz", b: "zzz"),
    TestStruct(a: "aaa", b: nil)
]

let output = input.sorted { lhs, rhs in
    if lhs.a != rhs.a { return lhs.a < rhs.a } // first sort by a
    if lhs.b != rhs.b { return lhs.b < rhs.b } // then sort by b

    return true
}

print(output)
//[
//    TempCode.TestStruct(a: Optional("aaa"), b: nil),
//    TempCode.TestStruct(a: Optional("xxx"), b: Optional("yyy")),
//    TempCode.TestStruct(a: Optional("zzz"), b: Optional("zzz"))
//]

*I made TestStruct a struct just so I can use the synthesized initializer and automatic print behaviour. It'll work just the same if it's a class.

If you'll be comparing TestStruct a lot, and it makes sense for there to be a standard order to comparing it, then it's best you add conformance to Comparable. Doing so also requires conformance to Equatable:

extension TestStruct: Equatable {
    public static func ==(lhs: TestStruct, rhs: TestStruct) -> Bool {
        return
            lhs.a == rhs.a &&
            lhs.b == rhs.b
    }
}

extension TestStruct: Comparable {
    public static func <(lhs: TestStruct, rhs: TestStruct) -> Bool {
        if lhs.a != rhs.a { return lhs.a < rhs.a } // first sort by a
        if lhs.b != rhs.b { return lhs.b < rhs.b } // then sort by b

        return true
    }
}

Now you can sort your array easily with the sorting behaviour specified by TestStruct:

let output = input.sorted()

You can see it in action here.

Upvotes: 2

GetSwifty
GetSwifty

Reputation: 7756

I believe this should cover the situations you want without having to iterate more than once (which is expensive)

array.sort { (lessThan, item) -> Bool in

    switch (lessThan.a, lessThan.b, item.a, item.b) {
    // if bs exist, use that comparison
    case (_, .some(let lessThanB), _, .some(let itemB)):
        return lessThanB < itemB
    // if bs don't exist by as do, use that comparison
    case (.some(let lessThanA), .none, .some(let itemA), .none):
        return lessThanA < itemA
    // if one item has a value but the other doesn't, the item with the value should be first
    case (_, .some(_), _, .none), (.some(_), _, .none, _ ):
        return true

    default:
        return false
    }
}

Upvotes: 3

dfrib
dfrib

Reputation: 73206

In case you needn't necessarily sort in place, you could simply apply the "algorithm" you describe in a brute-force fashion to sort array using several filter followed sorted for the subset arrays.

"Algorithm":

  • Primarily sort by b property, given that it is not nil.
  • For elements where the b property is nil, sort by a property.
  • Leave elements where both a and b are nil at the end of the sorted array.

E.g.:

// lets set up a more interesting example
let first = TestArray(a: "xxx", b: "yyy")
let second = TestArray(a: "ccc", b: nil)
let third = TestArray(a: "aaa", b: nil)
let fourth = TestArray(a: "zzz", b: "zzz")
let fifth = TestArray(a: "bbb", b: nil)

var array = [first, second, third, fourth, fifth]

let sorted = array.filter { $0.b != nil }.sorted { $0.b! < $1.b! } +
             array.filter { $0.b == nil && $0.a != nil }.sorted { $0.a! < $1.a! } +
             array.filter { $0.b == nil && $0.a == nil }

sorted.enumerated().forEach { print("\($0): b: \($1.b), a: \($1.a)") }
/* 0: b: Optional("yyy"), a: Optional("xxx")
   1: b: Optional("zzz"), a: Optional("zzz")
   2: b: nil, a: Optional("aaa")
   3: b: nil, a: Optional("bbb")
   4: b: nil, a: Optional("ccc")             */

Upvotes: 1

Related Questions