Search code examples
iosswiftrangensattributedstringnsrange

Getting ranges of duplicate substrings from string


I'm losing my mind over NSRange, RangeExpression and NSAttributedString functions. What I'm trying to achieve is to get all the ranges of a substring occurring in a text. What I ended up with is this:

let ranges: [NSRange] = textsToChange.compactMap { textToChange in
    if textToChange.result != nil {
        let strNumber: NSString = textFieldText as NSString
        return (strNumber).range(of: textToChange.text)
    } else {
        return nil
    }
}

I'm trying to map textsToChange array containing TextToChange objects (TextToChange is simply an object containing three fields: text, value and result, all being a String. Text is the only important here however) to NSRanges by finding each text in Textfield's text. Everything goes as expected when every contained text of textsToChange is unique. However if textsToChange contains any duplicates, let's say:

"car", "car", "bicycle"

Then, when having a textField text like:

"I own a yellow car, a blue car and a green bicycle"

I get ranges {15, 3}, {15, 3} and {43, 7} because rangeOf returns the range of the first occurrence of the word. Is there any simple function you know of to get distinctive ranges of the given substrings (So: {15, 3}, {27, 3} and {43, 7})?


Solution

  • You might try using Regular Expressions...

    func rangesForWords(_ words: [String], in text: String) -> [NSRange]? {
        
        var ranges: [NSRange] = []
        
        let ptrn = words.joined(separator: "|")
        
        do {
            let regex = try NSRegularExpression(pattern: ptrn, options: [.caseInsensitive])
            let items = regex.matches(in: text, options: [], range: NSRange(location: 0, length: (text as NSString).length))
            ranges = items.map{$0.range}
        } catch {
            // regex was bad?
            return nil
        }
        
        return ranges
    
    }
    

    and it can be called like this:

    let text: String = "I own a yellow car, a blue car and a green bicycle"
    
    let words: [String] = [
        "car",
        "bicycle",
    ]
        
    if let ranges = rangesForWords(words, in: text) {
        print(ranges)
    } else {
        print("rangesForWords returned nil")
    }
        
    

    which gives me this output:

    [{15, 3}, {27, 3}, {43, 7}]