How can I define custom operators to deal with Range of String.Index in Swift?

Issue

I find using String.Index really needs a lot of code in Swift, especially when it comes to methods of Range Swift doesn’t have. Like in above code where I don’t want an open range (both bounds exclusive). So I wonder if I can extend String or Range or something to simplify it. In the following code, I already know that startIndex and endIndex are of type String.Index, and startIndex..<endIndex is of type Range<String.Index>. But when I extend String.Index, I’d like to define a method like static func >.< (lhs: String.Index, rhs: String.Index) -> Range<String.Index>, but I failed because there’s no method to step String.Index up or down.

let startIndex = str.index(str.firstIndex(of: "[") ?? str.startIndex, offsetBy: 1)
let endIndex = str.firstIndex(of: "]") ?? str.startIndex
let subStr = str[startIndex..<endIndex]

I want to define operators like below. To clarify them using interval notation, if native method a...b is equivalent to [a, b] and a..<b is equivalent to [a, b), what is the equivalent of (a, b) and (a,b].

let startIndex = str.firstIndex(of: "[") ?? str.startIndex
let endIndex = str.firstIndex(of: "]") ?? str.startIndex
let subStr1 = str[startIndex...endIndex]    // [a, b]
let subStr3 = str[startIndex..<endIndex]    // [a, b)
let subStr2 = str[startIndex>.<endIndex]    // (a, b)
let subStr4 = str[startIndex>..endIndex]    // (a, b]

Solution

Edit: See the second code example instead! Please disregard the first block; I didn’t read the question as carefully as I should have, so it is not a relevant solution.

Edit 2: Also look at the first comment below this answer for an example of a major caveat of the first example. It does not work correctly for strings which contain emojis. Credit to Leo Dabus for this discovery.


This code might do what you’re looking for, but I can’t guarantee its reliability in all cases.

What it does is take a regular half-open range of integers (of the form a..<b) and a specific string (because, as matt mentioned in his comment, Swift string indices don’t pertain to the class String, only to some particular string), and returns an open range of indices for that particular string.

In effect, it just adds 1 to the lower bound to change it from inclusive to exclusive.

import Foundation

// Returns a Range of 'String.Index'es where both upper and lower bounds are exclusive.
extension Range {
    // Here, the upper bound of the range was already exclusive, so don't subtract 1 from it.  Only add 1 to the lower bound.
    static func OpenRange(string: String, range: Range<Int>) -> Range<String.Index> {
        return Range<String.Index>(uncheckedBounds: (String.Index(utf16Offset: range.lowerBound + 1, in: string), String.Index(utf16Offset: range.upperBound, in: string)))
    }
}

let theString = "abcdefg"
let endIndex = theString.index(theString.startIndex, offsetBy: 5)
let range: Range<String.Index> = theString.startIndex..<endIndex

let openRange = Range<String.Index>.OpenRange(string: theString, range: 0..<5)
print(theString[range]) // prints 'abcde'
print(theString[openRange]) // prints 'bcde'

Reference: https://www.avanderlee.com/swift/ranges-explained/

The example I had above doesn’t really fit the specific case you were asking about though, as I was starting from code I already had and was trying to ‘adapt’ it to this situation. My bad! I believe this is a better alternative:

// Returns a Range of 'String.Index'es between two specified characters in the string (as exclusive bounds)
extension Range {
    // Here, the upper bound of the range was already exclusive, so don't subtract 1 from it.  Only add 1 to the lower bound.
    static func OpenRange(string: String, from: String, to: String) -> Range<String.Index> {
        // The 'firstIndex' method expects a Character, not a String, so you need a type cast here
        let lowerInclusive = string.firstIndex(of: Character(from))!
        let lowerExclusive = string.index(lowerInclusive, offsetBy: 1)
        let upperExclusive = string.firstIndex(of: Character(to))!
        return Range<String.Index>(uncheckedBounds: (lowerExclusive, upperExclusive))
    }
}

let theString = "[10:02.11]"
let openRange1 = Range<String.Index>.OpenRange(string: theString, from: "[", to: ":")
let openRange2 = Range<String.Index>.OpenRange(string: theString, from: ":", to: "]")

print(theString[openRange1]) // prints '10'
print(theString[openRange2]) // prints '02.11'

Answered By – Quack E. Duck

Answer Checked By – Timothy Miller (AngularFixing Admin)

Leave a Reply

Your email address will not be published.