Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-02-16

Array subscript with multiple parameters

I needed to build an array of double values that ran as a cache next to a string. That is, the string would hold, say, 10 characters, then the array would hold 10 doubles, one for each character.

Like this:

let str = "1234567890"
let arr = Array<Double>(count: countElements(str), repeatedValue: 0.0)

When I need to lookup a value in the array, I have a String.Index and need to convert it to an Int every time I need to lookup a value, like this:

for var i = str.startIndex; i < str.endIndex; i = i.successor() {
    println(arr[distance(str.startIndex, i)])
}

This works, but it looks horrible. Every time I need to access the array, I need to use the distance operator.

It would be nice if I could use the String.Index as the index in the array also, like this:

extension Array {
    
    subscript (index: String.Index) -> T {
        set {
            self[distance("".startIndex, index)] = newValue
        }
        get {
            return self[distance("".startIndex, index)]
        }
    }
}

But a String.Index always stays associated with the string that it is derived from. You can check this with the following code:

let str1 = "1234567890"
let str2 = "12"
var j = str2.startIndex
var c = str[j]
j = j.successor()
c = str[j]
j = j.successor()
c = str[j]

The above will "EXC_BAD_INSTRUCTION" on us when 'j' is incremented beyond the endIndex of its associated string.

Thus the first implementation of the String.Index accessor above will not work. The first variable in the 'distance' call will immediately throw an EXC_BAD_INSTRUCTION when we try to increment beyond String.startIndex.

To my surprise however, it is possible to use multiple parameters in a subscript accessor, like this:

extension Array {
    
    subscript (string: String, index: String.Index) -> T {
        set {
            self[distance(string.startIndex, index)] = newValue
        }
        get {
            return self[distance(string.startIndex, index)]
        }
    }
}


let str = "1234567890"
let arr = Array<Double>(count: countElements(str), repeatedValue: 0.0)

for var i = str.startIndex; i < str.endIndex; i = i.successor() {
    println(arr[str, i])
}

After I tried this, I looked it up, and it is indeed documented that multiple parameters are possible in a subscript accessor. It is also written in the documentation that Swift will not copy a string unless it has to, so there should be no penalty associated with using the string itself in the subscript accessor instead of the slightly uglier looking String.startIndex.

It would be nice if it were possible to use only String.Index when indexing an element. Unfortunately it is not possible to subclass Array as it is defined as a struct. So if we want to use a single parameter index we need to define a wrapper and pass-through the relevant calls. Like this:

class StringIndexArray<T> {
    
    var array: Array<T> = []
    var startIndex: String.Index
    
    init(forString: String) {
        self.startIndex = forString.startIndex
    }
    
    subscript (index: String.Index) -> T {
        set {
            array[distance(startIndex, index)] = newValue
        }
        get {
            return array[distance(startIndex, index)]
        }
    }
    
    // pass through of relevant calls

    func append(value: T) { array.append(value) }

}

Update 2015.02.19: The above implementation of StringIndexArray will fail if the string it is created from is updated later. See my next blog about "That pesky String.Index, part 2"

Update 2015.05.07: When the presented Array extension is used, be aware that the String.endIndex may "count" too many characters when "Combining" characters (unicode \u + CC + xx) are present in the base string.

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment