12

I have a string for example "ab ad adk fda kla kad ab ab kd". I want to get all range of ab.(Here ab is present at 3 position so I should get 3 range).In normal scenarion my code is working fine, but if search text is ".",then I am getting wrong result

do {
    let regEx = try NSRegularExpression(pattern: searchText, options: NSRegularExpressionOptions.CaseInsensitive)

    let matchesRanges = regEx.matchesInString(attributedText.string, options:[], range: NSMakeRange(0, attributedText.string.length))

    for rng in matchesRanges {
        let wordRange = rng.rangeAtIndex(0)
    }
} catch {
    ...
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044

5 Answers5

21

The following solution uses the native Swift 4 function range(of:, options:, range:, locale:)):

extension String {
    func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range<Index>] {
        var ranges: [Range<Index>] = []
        while ranges.last.map({ $0.upperBound < self.endIndex }) ?? true,
            let range = self.range(of: substring, options: options, range: (ranges.last?.upperBound ?? self.startIndex)..<self.endIndex, locale: locale)
        {
            ranges.append(range)
        }
        return ranges
    }
}

(Swift 4 then provides native API to convert from Range<Index> to NSRange)

Stéphane Copin
  • 1,888
  • 18
  • 18
  • 2
    This can result in an infinite loop in some situations when using a regular expression. Check this answer on how to avoid it https://stackoverflow.com/a/32306142/2303865 – Leo Dabus Mar 04 '18 at 01:23
  • @LeoDabus I've identified one case where that could happen, as indeed some regular expression **will** return a valid range, even if the range to search in is empty. I've updated the code to reflect that. If that was not your original issue, would you mind sharing the regex that caused the issue? – Stéphane Copin Feb 06 '19 at 17:40
  • If I remember correctly it happens if there is an empty range in the end of the string. Sorry but the question/regex I couldn't find. – Leo Dabus Feb 06 '19 at 18:37
  • @LeoDabus I see, no worries, the edit I made should be good to fix the issue then ;) – Stéphane Copin Feb 06 '19 at 19:33
18

Swift 5:

The improved version of the most popular answer:

extension String {    
    func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range<Index>] {
        var ranges: [Range<Index>] = []
        while let range = range(of: substring, options: options, range: (ranges.last?.upperBound ?? self.startIndex)..<self.endIndex, locale: locale) {
            ranges.append(range)
        }
        return ranges
    }
}
Dmitry
  • 527
  • 5
  • 13
4

I would suggest such a solution:

import Foundation

extension String {

    func rangesOfString(s: String) -> [Range<Index>] {
        let re = try! NSRegularExpression(pattern: NSRegularExpression.escapedPatternForString(s), options: [])
        return re.matchesInString(self, options: [], range: nsRange(startIndex ..< endIndex)).flatMap { range($0.range) }
    }

    func range(nsRange : NSRange) -> Range<Index>? {
        let utf16from = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
        let utf16to   = utf16from.advancedBy(nsRange.length, limit: utf16.endIndex)

        if let from = String.Index(utf16from, within: self),
           let to   = String.Index(utf16to,   within: self)
        {
            return from ..< to
        } else {
            return nil
        }
    }

    func nsRange(range : Range<Index>) -> NSRange {
        let utf16from = String.UTF16View.Index(range.startIndex, within: utf16)
        let utf16to   = String.UTF16View.Index(range.endIndex,   within: utf16)
        return NSRange(location: utf16.startIndex.distanceTo(utf16from), length: utf16from.distanceTo(utf16to))
    }

}

print("[^x]? [^x]? [^x]?".rangesOfString("[^x]?")) // [Range(0..<5), Range(6..<11), Range(12..<17)]

Aside the main question, this code also shows the way to convert NSRange to and from Range<String.Index> (based on this post).

Community
  • 1
  • 1
werediver
  • 4,667
  • 1
  • 29
  • 49
  • Does it take care of all other special characters.? –  Apr 26 '16 at 13:54
  • I use [NSRegularExpression.escapedPatternForString()](https://developer.apple.com/library/mac/documentation/Foundation/Reference/NSRegularExpression_Class/#//apple_ref/occ/clm/NSRegularExpression/escapedPatternForString:) to escape any possible pattern metacharacters. This must be a reliable solution. – werediver Apr 26 '16 at 13:57
  • Those Range <-> NSRange conversions look familiar http://stackoverflow.com/a/30404532/1187415 :) – Martin R Apr 27 '16 at 07:03
  • That is probably the right origin. I've added the reference to the post. – werediver Apr 27 '16 at 07:29
1

You are using regular expressions, so you need to take care about characters that have special meaning - . is only one of them.

If you're doing a search for substrings, I suggest to use the good old rangeOf... methods instead:

func rangeOfString(_ searchString: String,
           options mask: NSStringCompareOptions,
             range searchRange: NSRange) -> NSRange

Just keep calling that method on your string (and adjust the searchRange), until no further matches are found.

Eiko
  • 25,601
  • 15
  • 56
  • 71
0

You can get occurance count for particular string by following code:

let str: NSMutableString = "ab ad adk fda kla kad ab ab kd"
let count = str.replaceOccurrencesOfString("ab", withString: "ab", options: NSStringCompareOptions.LiteralSearch, range: NSMakeRange(0, str.length))
Yuvrajsinh
  • 4,536
  • 1
  • 18
  • 32
  • 3
    No I want to get the ranges , so that can apply attributes on that ranges. –  Apr 26 '16 at 13:06