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 {
    ...
}
4
  • You need to show what you've tried and explain what it does wrong Commented Apr 26, 2016 at 12:50
  • please explain in detail what result you want ? Commented Apr 26, 2016 at 12:54
  • A . is a special character meaning 'Any character' Commented Apr 26, 2016 at 12:59
  • Do you know what regular expressions are? This is not plain old character-by-character string search... Commented Apr 26, 2016 at 13:20

5 Answers 5

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)

Sign up to request clarification or add additional context in comments.

4 Comments

This can result in an infinite loop in some situations when using a regular expression. Check this answer on how to avoid it stackoverflow.com/a/32306142/2303865
@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?
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.
@LeoDabus I see, no worries, the edit I made should be good to fix the issue then ;)
21

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

Comments

5

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).

4 Comments

Does it take care of all other special characters.?
I use NSRegularExpression.escapedPatternForString() to escape any possible pattern metacharacters. This must be a reliable solution.
Those Range <-> NSRange conversions look familiar stackoverflow.com/a/30404532/1187415 :)
That is probably the right origin. I've added the reference to the post.
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.

Comments

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

1 Comment

No I want to get the ranges , so that can apply attributes on that ranges.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.