Protect yourself: Retrieving penultimate values from arrays in Swift


The crashers

One of the first thoughts that comes to the mind when retrieving penultimate values is to count the array and work backwards.
let values = ["2","4","6"]
values[values.count - 2] // crashes if count is less than 2
values[values.endIndex.predecessor().predecessor()] // an alternative way to do the same thing, also crashes if count is less than 2
If the array contains only one element, however, the app will crash. Similarly if we reversed the array and worked forwards, an array of 1 item or less would again crash the attempt to retrieve a penultimate value.
let values = ["2","4","6"]
values.reverse()[1] // crashes if count is less than 2
The reason for the crash is that, as with any array, an attempt to retrieve a value beyond its bounds the result is exactly this: a crash.

Protected but error prone

One form of protection against crashes is to use advance and to ensure we don't advance beyond the bounds of the startIndex.
let values = ["2","4","6"]
values[advance(values.endIndex,-2,values.startIndex)] // returns last value if count less than 2
The problem is that if the array is empty then this approach will still crash, and if you have only 1 item in the array then that will be the value returned. An alternative approach is to use suffix() and .first.
let values = ["2","4","6"]
values.suffix(2).first // will return last value if count less than 2 as an optional
Or some whackier approach along the same lines:
let values = ["2","4","6"]
values.reverse().prefix(2).last // will return last value if count less than 2 as an optional
But when suffix() or prefix() go beyond the array count they return the entire array, so an array with a single value will once again return the erroneous (solitary) value using the above approaches and only return nil if the array is empty. (Note: In each case it is an optional value that is returned.)

Optionals

Of course all of the above approaches could be protected from errors if we first counted the array and ensured that the count exceeded 1, but there are other approaches that will only return a value if the array exceeds a count of 1, and otherwise return nil, which do not need our intervention. These include, the most concise (and the overall star player when it comes to simple code):
let values = ["2","4","6"]
values.dropLast().last // returns nil if count is less than 2
The more cumbersome.
let values = ["2","4","6"]
var generator = values.reverse().generate()
generator.next()
generator.next() // returns nil if count less than 2
And finally the mutating.
var values = ["2","4","6"]
values.popLast()
values.last // returns nil if count is less than 2

Other options for optionals

The dropLast().last combo prevents us from writing the more lengthy:
let result:String? = values.count > 1 ? values[values.count - 2] : nil
Or worse, the error prone:
let result:String? = values.isEmpty ? nil : values[max(0,values.count - 2)]
Which fails when there is a single value in the array.

The chosen path to penultimate

With knowledge of the dropLast().last combo in hand we can now write an Array extension
extension Array {
    var penultimate:Element? {
        return dropLast().last
    }
}
which makes life so much easier when retrieving penultimate values:
let values = ["2","4","6","7"]
values.penultimate
Although I have my suspicions that those wishing to optimise the time taken to retrieve penultimate values might well favour:
extension Array {
    var penultimate:Element? {
        if count <= 1 {return nil}
        return self[count-2]
    }
}
Or indeed (in the form written earlier):
extension Array {
    var penultimate:Element? {
        return count <= 1 ? nil : self[count-2]
    }
}
This is due to the O(self.count) complexity of dropLast(), measured in Big-O notation, which doesn't look good when compared to what we should expect from simply accessing a value at an index. And so with a final twist we're back to near where we began, the primitive option being seemingly the best (albeit less flashy and less concise).

Note: As I understand it (reading around) we shouldn't need to worry about count in evaluating the complexity. This is a property rather than an action. A stored value, not a working through of each and every item in an array.


Endorse on Coderwall

Comments