Expanding the structure quickly adds an initializer
I am trying to add an initializer to Range
.
import Foundation
extension Range {
init(_ range: NSRange, in string: String) {
let lower = string.index(string.startIndex, offsetBy: range.location)
let upper = string.index(string.startIndex, offsetBy: NSMaxRange(range))
self.init(uncheckedBounds: (lower: lower, upper: upper))
}
}
But the last line has a Swift compiler error.
Cannot convert value of type '(lower: String.Index, upper: String.Index)' (aka '(lower: String.CharacterView.Index, upper: String.CharacterView.Index)') to expected argument type '(lower: _ , upper: _) '
How do I compile it?
source to share
The problem is even that it String.Index
conforms to the protocol Comparable
, you still need to specify the type of range you want to work withpublic struct Range<Bound> where Bound : Comparable {}
Note . Since it NSString
uses UTF-16, check this and also in the link you mentioned, your original code does not work correctly for characters consisting of more than one UTF-16 code point. Below is an updated working version for Swift 3.
extension Range where Bound == String.Index {
init(_ range: NSRange, in string: String) {
let lower16 = string.utf16.index(string.utf16.startIndex, offsetBy: range.location)
let upper16 = string.utf16.index(string.utf16.startIndex, offsetBy: NSMaxRange(range))
if let lower = lower16.samePosition(in: string),
let upper = upper16.samePosition(in: string) {
self.init(lower..<upper)
} else {
fatalError("init(range:in:) could not be implemented")
}
}
}
let string = "βοΈLet it snow! βοΈ"
let range1 = NSRange(location: 0, length: 1)
let r1 = Range<String.Index>(range1, in: string) // βοΈ
let range2 = NSRange(location: 1, length: 2)
let r2 = Range<String.Index>(range2, in: string) // fatal error: init(range:in:) could not be implemented
To answer OP's comment . The problem is that the NSString object encodes a Unicode-compatible text string, represented as a sequence of UTF-16 code blocks. The Unicode scan values ββthat make up the contents of strings can be up to 21 bits in length. Longer scalar values ββmay require two UInt16 values ββto store.
So some letters like βοΈ take two UInt16 values ββin NSString, but only one in String. When you pass an NSRange argument to an initializer, you can expect it to work correctly in an NSString.
In my example, the results for r1
and r2
after conversion string
to utf16 are "βοΈ" and a fatal error. Meanwhile, the results of your original solution are "L" and "Le", respectively. Hope you can see the difference.
If you insist on a solution without utf16 conversion, you can take a look at the Swift source code to make a decision. In Swift 4, you will have an initializer as an inline lib. The code looks like this.
extension Range where Bound == String.Index {
public init?(_ range: NSRange, in string: String) {
let u = string.utf16
guard range.location != NSNotFound,
let start = u.index(u.startIndex, offsetBy: range.location, limitedBy: u.endIndex),
let end = u.index(u.startIndex, offsetBy: range.location + range.length, limitedBy: u.endIndex),
let lowerBound = String.Index(start, within: string),
let upperBound = String.Index(end, within: string)
else { return nil }
self = lowerBound..<upperBound
}
}
source to share
You need to constrain your range initializer to where Bound is String.Index, get NFSange utf16 indices, and find the same string index position in your string as shown below:
extension Range where Bound == String.Index {
init?(_ range: NSRange, in string: String) {
guard
let start = string.utf16.index(string.utf16.startIndex, offsetBy: range.location, limitedBy: string.utf16.endIndex),
let end = string.utf16.index(string.utf16.startIndex, offsetBy: range.location + range.length, limitedBy: string.utf16.endIndex),
let startIndex = start.samePosition(in: string),
let endIndex = end.samePosition(in: string)
else {
return nil
}
self = startIndex..<endIndex
}
}
source to share
The signature for this method requires the type "Linked" (at least in swift 4)
Since Bound is only an associated type "Comparable" and String.Index matches it, you just have to distinguish it.
extension Range {
init(_ range: NSRange, in string: String) {
let lower : Bound = string.index(string.startIndex, offsetBy: range.location) as! Bound
let upper : Bound = string.index(string.startIndex, offsetBy: NSMaxRange(range)) as! Bound
self.init(uncheckedBounds: (lower: lower, upper: upper))
}
}
https://developer.apple.com/documentation/swift/rangeexpression/2894257-bound
source to share