'subscript' is unavailable: cannot subscript String with a CountableClosedRange<Int>, see the documentation comment for discussion
Asked Answered
S

9

81

In Swift 4, I'm getting this error when I try to take a Substring of a String using subscript syntax.

'subscript' is unavailable: cannot subscript String with a CountableClosedRange, see the documentation comment for discussion

For example:

let myString: String = "foobar"
let mySubstring: Substring = myString[1..<3]

Two questions:

  1. How can I resolve this error?
  2. Where is "the documentation comment for discussion" that was referred to in the error?
Serpasil answered 4/8, 2017 at 3:12 Comment(3)
@KrisRoofe Somebody correct me if I'm wrong, but I think this is due to Extended Grapheme Clusters used to achieve native Unicode support. The swift Strings and Characters documentation states: >Every instance of Swift’s Character type represents a single extended grapheme cluster. An extended grapheme cluster is a sequence of one or more Unicode scalars that (when combined) produce a single human-readable character. docs.swift.org/swift-book/LanguageGuide/…Serpasil
You should make simple things simple and complicated things possible. Many times Apple makes simple things complicated in order to make complicated things possible.Scalariform
I hope Apple make string[0] become possible in the future.Applewhite
T
104
  1. If you want to use subscripts on Strings like "palindrome"[1..<3] and "palindrome"[1...3], use these extensions.

Swift 4

extension String {
    subscript (bounds: CountableClosedRange<Int>) -> String {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(startIndex, offsetBy: bounds.upperBound)
        return String(self[start...end])
    }

    subscript (bounds: CountableRange<Int>) -> String {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(startIndex, offsetBy: bounds.upperBound)
        return String(self[start..<end])
    }
}

Swift 3

For Swift 3 replace with return self[start...end] and return self[start..<end].

  1. Apple didn't build this into the Swift language because the definition of a 'character' depends on how the String is encoded. A character can be 8 to 64 bits, and the default is usually UTF-16. You can specify other String encodings in String.Index.

This is the documentation that Xcode error refers to.

More on String encodings like UTF-8 and UTF-16

Tribromoethanol answered 8/10, 2017 at 4:32 Comment(10)
All good, except that you should have used just bounds.upperBound instead of bounds.upperBound - bounds.lowerBound as offsetBy parameter.Chiastic
Offset should be bounds.upperBound - bounds.lowerBound. We expect "Palindrome"[3..<4] to give "i". But if we try let end = index(start, offsetBy: bounds.upperBound), we'll get "indr" instead.Tribromoethanol
My point here is that you want startIndex and endIndex to be indices, i.e. positions of characters in a string, not distances between the characters.Chiastic
Sorry, please discard the previous comment, I've accidentally pressed the button that should not have been pressed. Anyway, I see your point now. I was confused because you use self.startIndex parameter to initialize startIndex and startIndex parameter to initialize endIndex. If you use self.startIndex for both your bounds, you'll be able to just use bounds.upperBound for the end index offset. It would even be better to choose names different from String variables startIndex and endIndex for clarity. This will also allow you to drop all the self references.Chiastic
Are there changes for Swift 5?Serpasil
This could be improved with let end = index(start, offsetBy: bounds.count) instead of traversing from the beginning of the string twice. let end = index(start, offsetBy: bounds.count-1) in the CountableClosedRange case. index is O(n) where n is the offset from iSlipknot
even better this could be placed in an extension of StringProtocol and be applicable to Substrings, etc.Slipknot
You'll also need to add an extension that takes a CountablePartialRangeFrom<Int> if you'd like to write e.g. s[2...].Lemkul
It is not a solution. Swift also has "open ranges" like 1...Aden
Note that this will unnecessarily offset twice from the startIndex instead of just offsetting the range count from the resulting start to get the end. https://mcmap.net/q/23461/-get-nth-character-of-a-string-in-swiftOverthecounter
T
31

Your question (and self-answer) has 2 problems:

Subscripting a string with Int has never been available in Swift's Standard Library. This code has been invalid for as long as Swift exists:

let mySubstring: Substring = myString[1..<3]

The new String.Index(encodedOffset: ) returns an index in UTF-16 (16-bit) encoding. Swift's string uses Extended Grapheme Cluster, which can take between 8 and 64 bits to store a character. Emojis make for very good demonstration:

let myString = "🇺🇸🇨🇦🇬🇧🇫🇷"
let lowerBound = String.Index(encodedOffset: 1)
let upperBound = String.Index(encodedOffset: 3)
let mySubstring = myString[lowerBound..<upperBound]

// Expected: Canadian and UK flags
// Actual  : gibberish
print(mySubstring)

In fact, getting the String.Index has not changed at all in Swift 4, for better or worse:

let myString = "🇺🇸🇨🇦🇬🇧🇫🇷"
let lowerBound = myString.index(myString.startIndex, offsetBy: 1)
let upperBound = myString.index(myString.startIndex, offsetBy: 3)
let mySubstring = myString[lowerBound..<upperBound]

print(mySubstring)
Tallowy answered 12/8, 2017 at 13:27 Comment(0)
C
20

You could just convert your string to an array of characters...

let aryChar = Array(myString)

Then you get all the array functionality...

Camion answered 19/12, 2019 at 14:33 Comment(2)
And then convert to String back? This solution is so soAden
Just for completeness, map works fine too: "Hello World".map{$0}[6] // 'W'Howes
S
15
  1. How can I resolve this error?

This error means you can't use an Int in the subscript format – you have to use a String.Index, which you can initialize with an encodedOffset Int.

let myString: String = "foobar"
let lowerBound = String.Index.init(encodedOffset: 1)
let upperBound = String.Index.init(encodedOffset: 3)
let mySubstring: Substring = myString[lowerBound..<upperBound]
  1. Where is "the documentation comment for discussion" that was referred to in the error?

It's on GitHub in the Swift Standard Library repository in a file called UnavailableStringAPIs.swift.gyb in the bottom of a locked filing cabinet stuck in a disused lavatory with a sign on the door saying 'Beware of the Leopard'. link

Serpasil answered 4/8, 2017 at 3:12 Comment(5)
Don't use encodedOffset here; it doesn't necessarily correspond to characters, it (currently) corresponds to UTF-16 code units (which just happens to be characters for ASCII strings). For example, with let myString = "𐐷hello", mySubstring is just 𐐷 (because that's encoded with 2 UTF-16 code units). With let myString = "🇧🇪hello", the subscripting raises a runtime error, as 🇧🇪 is encoded with 4 UTF-16 code units. Instead of using encodedOffset:, use myString.index(myString.startIndex, offsetBy: 2); which talks in terms of characters.Polyphemus
(also note that String's ranged subscript returns Substring, not String in Swift 4)Polyphemus
@Polyphemus What if I want my lowerBound to be something higher than myString.startIndex? I've updated the example to not start at 0. I suppose I could initialize a Range using init(uncheckedBounds: (lower:upper:)) and take its lower and upper bounds. I've updated the example regarding Substring.Serpasil
To get an index at offset 1, you can say let lowerBound = myString.index(myString.startIndex, offsetBy: 1) or let lowerBound = myString.index(after: myString.startIndex).Polyphemus
I get the Hitchiker's Guide reference :)Chockablock
S
6

Based on p-sun's answer

Swift 4

extension StringProtocol {
    subscript(bounds: CountableClosedRange<Int>) -> SubSequence {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(start, offsetBy: bounds.count)
        return self[start..<end]
    }

    subscript(bounds: CountableRange<Int>) -> SubSequence {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(start, offsetBy: bounds.count)
        return self[start..<end]
    }
}

Notable changes:

  • Now an extension of StringProtocol. This allows adopters such as Substring to also gain these subscripts.
  • End indices are offset from the start index of the bounds rather than the start of the string. This prevents traversing from the start of the String twice. The index method is O(n) where n is the offset from i.
Slipknot answered 12/4, 2019 at 21:18 Comment(4)
Definitely an improvement on the accepted answer IMHO. You could also use max(0, bounds.lowerBound) as the offsetBy parameter when calculating the start index to ensure that you don't accidentally try to access a negative index.Henchman
I personally would prefer that to fail, as that is a developer error. Out of bound errors are expected with invalid indexes. If they are fixed silently I wouldn't catch the mistake.Slipknot
Why both methods are exactly the same... shouldn't one be ..< and second ... ?Selfrevealing
@GrzegorzKrukowski No, because the endIndex of the input already accounts for the difference.Slipknot
D
4

2022

Improved code of p-sun's and Justin Oroz's answers:

Code works with SubSequences, so it's uses less memory.

You're able to do:

// works well even on substrings
"01234567890"[i: 1]  // Character "1"
"01234567890"[i: 15] // nil

"01234567890"[safe: 1..<5] // subsequence "1234"
"01234567890"[safe: 1...5] // subsequence "12345"
"012"[safe: 1..<15]        // subsequence "12"
"012"[safe: 1...15]        // subsequence "12"


"012"[unsafe: 1..<9]       // will thrown FatalError OutOfBounds exception
"012"[unsafe: 1...9]       // will thrown FatalError OutOfBounds exception
"012"[unsafe: -1..<2]      // will thrown FatalError OutOfBounds exception
"012"[unsafe: -1...2]      // will thrown FatalError OutOfBounds exception
public extension StringProtocol {
    subscript(i idx: Int) -> Character? {
        if idx >= self.count { return nil }
        
        return self[self.index(self.startIndex, offsetBy: idx)]
    }
}

public extension Substring {
    subscript(i idx: Int) -> Character? {
        if idx >= self.count { return nil }
        return self.base[index(startIndex, offsetBy: idx)]
    }
}

public extension StringProtocol {
    /// Use this if you want to get OutOfBounds exception
    subscript(unsafe bounds: Range<Int>) -> SubSequence {
        let startIndex = index(self.startIndex, offsetBy: bounds.lowerBound)
        return self[startIndex..<index(startIndex, offsetBy: bounds.count)]
    }
    
    /// Use this if you want to get OutOfBounds exception
    subscript(unsafe bounds: ClosedRange<Int>) -> SubSequence {
        let startIndex = index(self.startIndex, offsetBy: bounds.lowerBound)
        return self[startIndex..<index(startIndex, offsetBy: bounds.count)]
    }
}

public extension String {
    /// Use this if you want to get result with any incorrect input
    subscript(safe bounds: CountableClosedRange<Int>) -> SubSequence {
        let lowerBound = max(0, Int(bounds.lowerBound) )
        
        guard lowerBound < self.count else { return "" }
        
        let upperBound = min(Int(bounds.upperBound), self.count-1)
        
        guard upperBound >= 0 else { return "" }
        
        let minIdx = index(startIndex, offsetBy: lowerBound )
        let maxIdx = index(minIdx, offsetBy: upperBound-lowerBound )
        
        return self[minIdx...maxIdx]
    }
    
    /// Use this if you want to get result with any incorrect input
    subscript(safe bounds: CountableRange<Int>) -> SubSequence {
        let lowerBound = max(0, bounds.lowerBound)
        
        guard lowerBound < self.count else { return "" }
        
        let upperBound = min(bounds.upperBound, self.count)
        
        guard upperBound >= 0 else { return "" }
        
        let minIdx = index(startIndex, offsetBy: lowerBound )
        let maxIdx = index(minIdx, offsetBy: upperBound-lowerBound )
        
        return self[minIdx..<maxIdx]
    }
}

Code is tested:

enter image description here

enter image description here

Dump answered 9/12, 2022 at 20:13 Comment(0)
H
3

Building on both p-sun's and Justin Oroz's answers, here are two extensions that protect against invalid indexes beyond the start and end of a string (these extensions also avoid rescanning the string from the beginning just to find the index at the end of the range):

extension String {

    subscript(bounds: CountableClosedRange<Int>) -> String {
        let lowerBound = max(0, bounds.lowerBound)
        guard lowerBound < self.count else { return "" }

        let upperBound = min(bounds.upperBound, self.count-1)
        guard upperBound >= 0 else { return "" }

        let i = index(startIndex, offsetBy: lowerBound)
        let j = index(i, offsetBy: upperBound-lowerBound)

        return String(self[i...j])
    }

    subscript(bounds: CountableRange<Int>) -> String {
        let lowerBound = max(0, bounds.lowerBound)
        guard lowerBound < self.count else { return "" }

        let upperBound = min(bounds.upperBound, self.count)
        guard upperBound >= 0 else { return "" }

        let i = index(startIndex, offsetBy: lowerBound)
        let j = index(i, offsetBy: upperBound-lowerBound)

        return String(self[i..<j])
    }
}
Henchman answered 26/4, 2019 at 3:11 Comment(1)
you can simply use the range count https://mcmap.net/q/23461/-get-nth-character-of-a-string-in-swiftOverthecounter
J
-2
extension String {

    subscript(bounds: CountableClosedRange<Int>) -> String {
        let lowerBound = max(0, bounds.lowerBound)
        guard lowerBound < self.count else { return "" }

        let upperBound = min(bounds.upperBound, self.count-1)
        guard upperBound >= 0 else { return "" }

        let i = index(startIndex, offsetBy: lowerBound)
        let j = index(i, offsetBy: upperBound-lowerBound)

        return String(self[i...j])
    }

    subscript(bounds: CountableRange<Int>) -> String {
        let lowerBound = max(0, bounds.lowerBound)
        guard lowerBound < self.count else { return "" }

        ***let upperBound = min(bounds.upperBound, self.count-1)***
        guard upperBound >= 0 else { return "" }

        let i = index(startIndex, offsetBy: lowerBound)
        let j = index(i, offsetBy: upperBound-lowerBound)

        return String(self[i..<j])
    }
}
Johnie answered 29/4, 2019 at 8:0 Comment(1)
Swift 5 - doesn't work with "open" ranges. Additionally what does your *** mean?Aden
C
-2

You getting this error because result of subscript with range is Substring? not Substring.

You must use following code:

let myString: String = "foobar"
let mySubstring: Substring? = myString[1..<3]
Currycomb answered 6/5, 2019 at 9:18 Comment(1)
Subscripting a string with Int has never been available in Swift's Standard Library. It's using String.Index as input. The same is about range of Int ofc. So this code has been invalid for as long as Swift exists.Dump

© 2022 - 2025 — McMap. All rights reserved.