2
let fullString = "Hello world, there are \(string(07)) continents and \(string(195)) countries."
let range = [NSMakeRange(24,2), NSMakeRange(40,3)]

Need to find the NSRange for numbers in the entire full string and there is a possibility that both numbers can be same. Currently hard coding like shown above, the message can be dynamic where hard coding values will be problematic.

I have split the strings and try to fetch NSRange since there is a possibility of same value. like stringOne and stringTwo.

func findNSMakeRange(initialString:String, fromString: String) {
        let fullStringRange = fromString.startIndex..<fromString.endIndex
        fromString.enumerateSubstrings(in: fullStringRange, options: NSString.EnumerationOptions.byWords) { (substring, substringRange, enclosingRange, stop) -> () in
            let start = distance(fromString.startIndex, substringRange.startIndex)
            let length = distance(substringRange.startIndex, substringRange.endIndex)
            let range = NSMakeRange(start, length)

            if (substring == initialString) {
                print(substring, range)
            }
        })
    }

Receiving errors like Cannot invoke distance with an argument list of type (String.Index, String.Index)

Anyone have any better solution ?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Karen
  • 169
  • 1
  • 16

1 Answers1

7

You say that you want to iterate through NSRange matches in a string so that you can apply a bold attribute to the relevant substrings.

In Swift 5.7 and later, you can use the new Regex:

string.ranges(of: /\d+/)
    .map { NSRange($0, in: string) }
    .forEach {
        attributedString.setAttributes(attributes, range: $0)
    }

Or if you find the traditional regular expressions too cryptic, you can import RegexBuilder, and you can use the new regex DSL:

string.ranges(of: Regex { OneOrMore(.digit) })
    .map { NSRange($0, in: string) }
    .forEach {
        attributedString.setAttributes(attributes, range: $0)
    }

In Swift versions prior to 5.7, one would use NSRegularExpression. E.g.:

let range = NSRange(location: 0, length: string.count)
try! NSRegularExpression(pattern: "\\d+").enumerateMatches(in: string, range: range) { result, _, _ in
    guard let range = result?.range else { return }
    attributedString.setAttributes(attributes, range: range)
}

Personally, before Swift 5.7, I found it useful to have a method to return an array of Swift ranges, i.e. [Range<String.Index>]:

extension StringProtocol {
    func ranges<T: StringProtocol>(of string: T, options: String.CompareOptions = []) -> [Range<Index>] {
        var ranges: [Range<Index>] = []
        var start: Index = startIndex
        
        while let range = range(of: string, options: options, range: start ..< endIndex) {
            ranges.append(range)
            
            if !range.isEmpty {
                start = range.upperBound               // if not empty, resume search at upper bound
            } else if range.lowerBound < endIndex {
                start = index(after: range.lowerBound) // if empty and not at end, resume search at next character
            } else {
                break                                  // if empty and at end, then quit
            }
        }
        
        return ranges
    }
}

Then you can use it like so:

let string = "Hello world, there are 09 continents and 195 countries."
let ranges = string.ranges(of: "[0-9]+", options: .regularExpression)

And then you can map the Range to NSRange. Going back to the original example, if you wanted to make these numbers bold in some attributed string:

string.ranges(of: "[0-9]+", options: .regularExpression)
    .map { NSRange($0, in: string) }
    .forEach { attributedString.setAttributes(boldAttributes, range: $0) }

Resources:

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I am passing `NSRange` to set bold attribute to the `Numbers` in string. – Karen Mar 09 '18 at 15:41
  • 1
    Yeah I did implemented the bold attributed part, problem was only hardcoding ranges. Your answer fixed the problem. Thanks. – Karen Mar 09 '18 at 16:13
  • This works but doesn't take into account multiple recurring strings combined in the same string. For example: Searching for `aa` in `aaa aaa` should bold all the text. Instead the above only bolds the first occurrences. This is due to starting from the upper bound of the previous range. – sam_smith Aug 14 '19 at 01:51
  • Note: Updating the code with this: `start = index(after: range.lowerBound)` captures all recurring strings even if they are contained in others. Making it slightly more accurate. – sam_smith Aug 14 '19 at 02:08
  • @simon_smiley - I wouldn’t your approach as as “more accurate”. For example, Apple’s own [`enumerateMatches(in:options:range:using:)`](https://developer.apple.com/documentation/foundation/nsregularexpression/1409687-enumeratematches) behaves like my answer above (where searching for “aa” in “aaaa” will return two results, not three). You might prefer the behavior you describe, and if it solves your particular problem, then that’s fine, do that. But I think your behavior is non-standard, not “more accurate”. – Rob Aug 14 '19 at 03:42
  • @simon_smiley - FWIW, if you want to search for occurrences of sequences of 2 or more `a` characters, you really should just use the aforementioned regex enumeration method but with `let regex = NSRegularExpression(pattern: "a{2,}")`. – Rob Aug 14 '19 at 03:42
  • I suppose it comes down to definition. I agree that my answer solves my particular problem. Neither solution can be considered always correct. Both answers provide a specific subset. In retrospect I should have left off my final sentence to make my comment more useful. – sam_smith Aug 14 '19 at 03:52
  • @Rob `where Index == String.Index` that constrain is not needed anymore. Btw `while let range = range(of: string, options: options, range: start.. – Leo Dabus Jan 23 '22 at 01:24
  • 1
    Thanks. Modified to remove unnecessary constraint and now handling edge-case that might result in zero length matches. – Rob Jan 23 '22 at 08:40