Find and return the ranges of all the occurrences of a given string in Swift

While talking about string pattern matching recognition problem, the first thought pop up in your mind would probably be Knuth–Morris–Pratt algorithm. However, this article, I would like to introduce a more intuitive method based on range(of:options:range:locale:) which is part of Swift Standard Library.
Return the first occurrence multiple times to collect all of them
If you look up Apple Developer Documentation, you will find the description of function range(of:options:range:locale:) which is identical to the quote below.
Finds and returns the range of the first occurrence of a given string within a given range of the String, subject to given options, using the specified locale, if any.
Yes, it only returns the first occurrence. Therefore, to fulfill our purpose, we need to construct a while-loop to iterate all the occurrences of a given string within the receiver. A reference snippet will be like the following code block. And note that an offset is also added to reduce the length of range in each iteration. So actually, the position of each loop is basically based on the upper bound of the previous iteration.
extension String {
func indices(of occurrence: String) -> [Int] {
var indices = [Int]()
var position = startIndex
while let range = range(of: occurrence, range: position..<endIndex) {
let i = distance(from: startIndex,
to: range.lowerBound)
indices.append(i)
let offset = occurrence.distance(from: occurrence.startIndex,
to: occurrence.endIndex) - 1
guard let after = index(range.lowerBound,
offsetBy: offset,
limitedBy: endIndex) else {
break
}
position = index(after: after)
}
return indices
}
}Transform indices to ranges
In applying above code to a real case, sometimes returning all indices is not enough to do find-and-replace-all operations in text editing. Therefore, a transform helper can help to generate a range array that is much easier to deal with substrings.
extension String {
func ranges(of searchString: String) -> [Range<String.Index>] {
let _indices = indices(of: searchString)
let count = searchString.characters.count
return _indices.map({ index(startIndex, offsetBy: $0)..<index(startIndex, offsetBy: $0+count) })
}
}Create NSRange from Range before Swift 4
If you have shifted your Xcode to version 9. The Swift standard library of Swift 4 provides a method to convert from Range<String.Index> to NSRange directly. It’s definitely a feature that many developers are looking forward to it because it’s still unavoidable to deal with NSRange sometimes when certain function calls are not fully migrated to native Swift yet, like an example of NSAttributedString below.
let str = "Hello, playground! Hello, world! Hello, earth!"
let attributedString = NSAttributedString(string: str)
attributedString.enumerateAttributes(in: NSRange(location: 0,
length: str.utf16.count),
options: NSAttributedString.EnumerationOptions.longestEffectiveRangeNotRequired) { (attributes, range, stop) in
}In order to have a smoother migration from the current version of Swift to Swift 4. We can implement a grammar compatible function to create NSRange from Range before Xcode 9 is released officially.
extension NSRange {
init(_ range: Range<String.Index>, in string: String) {
self.init()
let startIndex = range.lowerBound.samePosition(in: string.utf16)
let endIndex = range.upperBound.samePosition(in: string.utf16)
self.location = string.distance(from: string.startIndex,
to: range.lowerBound)
self.length = startIndex.distance(to: endIndex)
}
}Test above functions in the playground
Basically, all above codes are necessary elements we need to find and return the ranges of all the occurrences of a given string. Last, let’s test and see a result by the snippet below.
let str = "Hello, playground, Hello, playground, Hello, playground, Hello, playground, Hello, playground"
let indices = str.indices(of: "round")
print("[Int] : \(indices)")
let ranges = str.ranges(of: "playground")
for range in ranges {
print("`Range` : " + str.substring(with: range))
}
for range in ranges.map({ NSRange($0, in: str) }) {
print("`NSRange` : " + (str as NSString).substring(with: range))
}And the log console will be seen like this in the playground.
[Int] : [12, 31, 50, 69, 88]
`Range` : playground
`Range` : playground
`Range` : playground
`Range` : playground
`Range` : playground
`NSRange` : playground
`NSRange` : playground
`NSRange` : playground
`NSRange` : playground
`NSRange` : playground