78

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?
Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
Barry Jones
  • 1,329
  • 1
  • 9
  • 16
  • 2
    @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. https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html – Barry Jones Mar 28 '19 at 19:16
  • 12
    You should make simple things simple and complicated things possible. Many times Apple makes simple things complicated in order to make complicated things possible. – Bob Ueland Apr 14 '19 at 02:53
  • I hope Apple make string[0] become possible in the future. – Zhou Haibo Feb 27 '22 at 16:47

9 Answers9

102
  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

p-sun
  • 1,776
  • 1
  • 17
  • 10
  • 2
    All good, except that you should have used just `bounds.upperBound` instead of `bounds.upperBound - bounds.lowerBound` as `offsetBy` parameter. – Zmicier Zaleznicenka Nov 08 '17 at 13:14
  • 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. – p-sun Nov 09 '17 at 21:07
  • 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. – Zmicier Zaleznicenka Nov 10 '17 at 12:17
  • 1
    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. – Zmicier Zaleznicenka Nov 10 '17 at 12:24
  • Are there changes for Swift 5? – Barry Jones Mar 28 '19 at 19:18
  • 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 i – Justin Oroz Apr 12 '19 at 19:49
  • even better this could be placed in an extension of `StringProtocol` and be applicable to Substrings, etc. – Justin Oroz Apr 12 '19 at 21:11
  • You'll also need to add an extension that takes a CountablePartialRangeFrom if you'd like to write e.g. s[2...]. – Míng Oct 25 '19 at 02:10
  • It is not a solution. Swift also has "open ranges" like `1...` – Vyachaslav Gerchicov Nov 09 '20 at 07:16
  • 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://stackoverflow.com/a/24144365/2303865 – Leo Dabus Dec 26 '20 at 13:36
30

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)
Code Different
  • 90,614
  • 16
  • 144
  • 163
19

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

let aryChar = Array(myString)

Then you get all the array functionality...

camille
  • 16,432
  • 18
  • 38
  • 60
Ronk
  • 353
  • 2
  • 6
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

Barry Jones
  • 1,329
  • 1
  • 9
  • 16
  • 3
    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. – Hamish Aug 04 '17 at 10:57
  • (also note that `String`'s ranged subscript returns `Substring`, not `String` in Swift 4) – Hamish Aug 04 '17 at 11:00
  • 1
    @Hamish 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. – Barry Jones Aug 04 '17 at 14:38
  • 1
    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)`. – Hamish Aug 04 '17 at 14:50
  • 1
    I get the Hitchiker's Guide reference :) – Benj Jul 12 '19 at 15:24
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.
Justin Oroz
  • 614
  • 8
  • 14
  • 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. – Chris Frederick Apr 26 '19 at 02:11
  • 1
    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. – Justin Oroz Apr 26 '19 at 20:41
  • Why both methods are exactly the same... shouldn't one be ..< and second ... ? – Grzegorz Krukowski Aug 03 '22 at 21:16
  • @GrzegorzKrukowski No, because the endIndex of the input already accounts for the difference. – Justin Oroz Dec 19 '22 at 21:48
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])
    }
}
Chris Frederick
  • 5,482
  • 3
  • 36
  • 44
2

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

Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101
-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])
    }
}
vimuth
  • 5,064
  • 33
  • 79
  • 116
chan sam
  • 49
  • 3
-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]
Andrey M.
  • 3,021
  • 29
  • 42
  • 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. – Andrew_STOP_RU_WAR_IN_UA Aug 28 '23 at 23:28