Replacing characters in a parameter string

Raphael picture Raphael · Mar 3, 2017 · Viewed 9.2k times · Source

I want to wipe an input string. Let's start with this:

func foo(s: String) {
    s.replaceSubrange(0..<s.characters.count,
            with: String(repeating: "0", count: s.characters.count))
}

Predictably, this results in

cannot use mutating member on immutable value: 's' is a 'let' constant

Fine:

func foo(s: inout String) {
    s.replaceSubrange(0..<s.characters.count,
            with: String(repeating: "0", count: s.characters.count))
}

But now:

'inout String' is not convertible to 'String'

pointing to .character -- what?!

Oddly enough, when I do:

func foo(s: inout String) {
    let n = s.characters.count
    s.replaceSubrange(0..<n,
            with: String(repeating: "0", count: n))

}

the call to .characters is completely fine, but

cannot invoke 'replaceSubrange' with an argument list of type '(CountableRange, with: String)'

Using 0...n-1 doesn't work, either.

How can I replace characters in a parameter string?

Answer

Hamish picture Hamish · Mar 3, 2017

A String's CharacterView isn't indexed by Int, rather it is indexed by the opaque String.Index type. Therefore you would want to say:

func foo(s: inout String) {
    s.replaceSubrange(s.startIndex..<s.endIndex,
                      with: String(repeating: "0", count: s.characters.count))
}

We're not directly using the string's characters property to get the start or end index, as String provides for them in it's API for convenience – but they are just forwarded onto the CharacterView.

To replace different subranges, you would want to use the various indexing method, such as index(after:), index(before:) and index(_:offsetBy:) on String in order to offset the indices for the lower and upper bound of the range (these methods also just forward onto the string's character view). A good summary of these methods is given in this Q&A.

The reasoning behind not using Int as the index type is partly due to the fact that subscripting a String with an Int would present the illusion that indexing the string is a trivial operation – which is not always the case, due to the fact that characters can have different byte lengths, thus potentially making indexing an O(n) operation.

Although if your aim to just to set s to a string containing all zeroes with the same count as it's previous value, you could just do an assignment instead:

func foo(s: inout String) {
    s = String(repeating: "0", count: s.characters.count)
}