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?

+3


source to share


3 answers


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
  }
}

      

+1


source


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
    }
}

      

+1


source


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

0


source







All Articles