Maybe you have fun with this:
import Foundation
public struct IndexedString: CustomStringConvertible {
private let s: String
private let indices: [Int]
private let utf8: [UInt8]
public var description: String { s }
public var count: Int { indices.count }
public init(_ s: String) {
self.s = s
var index = 0
indices = s.map{ index += $0.utf8.count; return index }
utf8 = Array(s.utf8)
}
public subscript(position: Int) -> IndexedString {
return IndexedString(String(bytes: utf8[(position > 0 ? indices[position-1] : 0)..<indices[position]], encoding: .utf8)!)
}
public subscript(range: Range<Int>) -> IndexedString {
return IndexedString(String(bytes: utf8[(range.lowerBound > 0 ? indices[range.lowerBound-1] : 0)..<indices[range.upperBound-1]], encoding: .utf8)!)
}
public subscript(range: ClosedRange<Int>) -> IndexedString {
return IndexedString(String(bytes: utf8[(range.lowerBound > 0 ? indices[range.lowerBound-1] : 0)..<indices[range.upperBound]], encoding: .utf8)!)
}
public func replacing<Replacement>(_ regex: some RegexComponent, with replacement: Replacement, maxReplacements: Int = .max) -> Self where Replacement : Collection, Replacement.Element == Character {
IndexedString(s.replacing(regex, with: replacement, maxReplacements: maxReplacements))
}
}
// usage:
let s = IndexedString("Häl😉y\u{301}o")
print(s) // prints "Häl😉ýo"
for i in 0..<s.count {
print("\(i): \(s[i])") // prints "0: H", "1: ä", "2: l", "3: 😉", "4: ý", and "5: o"
}
print(s[1..<3]) // prints "äl"
print(s[1...3]) // prints "äl😉"
print(s.replacing(/[a-z]/, with: "x")) // prints "Häx😉ýx"