How to write my own extension to enable Python-like String indexing?

Learning to use Swift, PATs and Generics. Regardless of performance, I would like to make it so in my code, I can perform string indexing like Python:

let text = "hello"
print(text[0..<2]) // "he"

I am trying to implement it as such:

import Foundation

extension String {
    subscript (bounds: RangeExpression) -> String {
        ...
    }
}

I've tried a lot of things, like:

  • subscript (bounds: RangeExpression<Int>, but this is not allowed in Swift 5.6
  • define another protocol IntRange: RangeExpression, but I did not figure out how to set its associated type

What is the right approach to this? Thanks!

Will this work for you?

let text = "hello"
print(text[0..<2]) // "he"
print(text[0...2]) // "hel"
print(text[1...]) // "ello"
print(text[..<2]) // "he"
print(text[...2]) // "hel"

extension String {
    subscript(bounds: Range<Int>) -> String {
        String(self[self + bounds.lowerBound ..< self + bounds.upperBound])
    }
    subscript(bounds: ClosedRange<Int>) -> String {
        String(self[self + bounds.lowerBound ... self + bounds.upperBound])
    }
    subscript(bounds: PartialRangeFrom<Int>) -> String {
        String(self[self + bounds.lowerBound ..< endIndex])
    }
    subscript(bounds: PartialRangeUpTo<Int>) -> String {
        String(self[startIndex ..< self + bounds.upperBound])
    }
    subscript(bounds: PartialRangeThrough<Int>) -> String {
        String(self[startIndex ... self + bounds.upperBound])
    }
    static func + (string: Self, offset: Int) -> String.Index {
        string.index(at: offset)
    }
    func index(at offset: Int) -> String.Index {
        index(startIndex, offsetBy: offset)
    }
}

BTW, you may return Substring (and only convert to String on the use side if needed) although if performance doesn't matter then returning String is fine.

1 Like

Awesome, thanks! Out of curiosity, what exactly is the purpose of RangeExpression then?

It's very intentionally omitted.

While the ergonomics aren't great as far as peoples usual expectations, indexing strings is actually a surprisingly uncommon operation in real world code (i.e. not a "shuffle the characters of a string" style code challenge). Most algorithms are better expressed in terms of higher level concepts like slicing, joining, taking prefixes, dropping prefixes, etc.

7 Likes

Remember that index(_: String.Index, offsetBy:) is O(n) operation. If you do it in a loop for all characters in the string - you have a quadratic algorithm.

We can have it in a form:

text[slow: 0...2]

"slow" should discourage people from using it in real world apps.

I am not sure :thinking:. Will leave for others to comment upon.

3 Likes

RangeExpression serves as an abstraction over all the range types in @tera’s example code.

instead of providing overloads for Range, ClosedRange, PartialRangeFrom, PartialRangeUpTo, and PartialRangeThrough, you could just provide one generic RangeExpression method.

1 Like

Thank you, indeed:

let text = "hello"
print(text[slow: 0..<2]) // "he"
print(text[slow: 0...2]) // "hel"
print(text[slow: 1...]) // "ello"
print(text[slow: ..<2]) // "he"
print(text[slow: ...2]) // "hel"

extension String {
    subscript<T: RangeExpression>(slow bounds: T) -> String where T.Bound == Int {
        let range = bounds.relative(to: 0 ..< .max)
        let left = index(startIndex, offsetBy: range.lowerBound)
        let right = range.upperBound == .max ? endIndex : index(startIndex, offsetBy: range.upperBound)
        return String(self[left ..< right])
    }
}
1 Like