Pure Swift: A method for replacing occurrences of a string within a string (updated for Xcode 6.3.1, Swift 1.2 and Xcode 7 beta 1, Swift 2)
rangesOfString
In the last blogpost, I created an extension to the String type to add a rangesOfString: method that was written purely in Swift.extension String { func rangesOfString(findStr:String) -> [Range<String.Index>] { var arr = [Range<String.Index>]() var startInd = self.startIndex // check first that the first character of search string exists if contains(self, first(findStr)!) { // if so set this as the place to start searching startInd = find(self,first(findStr)!)! } else { // if not return empty array return arr } var i = distance(self.startIndex, startInd) while i<=count(self)-count(findStr) { if self[advance(self.startIndex, i)..<advance(self.startIndex, i+count(findStr))] == findStr { arr.append(Range(start:advance(self.startIndex, i),end:advance(self.startIndex, i+count(findStr)))) i = i+count(findStr) } else { i++ } } return arr } } // try further optimisation by jumping to next index of first search character after every find "a very good hello, hello".rangesOfString("hello”)
stringByReplacingOccurrencesOfString
With the rangesOfString: method in placed we can create a new stringByReplacingOccurrencesOfString: method again written purely in Swift:func stringByReplacingOccurrencesOfString(string:String, replacement:String) -> String { // get ranges first using rangesOfString: method, then glue together the string using ranges of existing string and old string let ranges = self.rangesOfString(string) // if the string isn't found return unchanged string if ranges.isEmpty { return self } var newString = "" var startInd = self.startIndex for r in ranges { newString += self[startInd..<minElement(r)] newString += replacement if maxElement(r) < self.endIndex { startInd = advance(maxElement(r),1) } } // add the last part of the string after the final find if maxElement(ranges.last!) < self.endIndex { newString += self[advance(maxElement(ranges.last!),1)..<self.endIndex] } return newString }And since this is a String method it overrides the Foundation method of the same name when placed inside a String extension.
Updated: Code for Swift 2 (stringByReplacingOccurencesOfString)
extension String { func rangesOfString(findStr:String) -> [Range] { var arr = [Range<String.Index>]() var startInd = self.startIndex // check first that the first character of search string exists if self.characters.contains(findStr.characters.first!) { // if so set this as the place to start searching startInd = self.characters.indexOf(findStr.characters.first!)! } else { // if not return empty array return arr } var i = distance(self.startIndex, startInd) while i<=self.characters.count-findStr.characters.count { if self[advance(self.startIndex, i)..<advance(self.startIndex, i+findStr.characters.count)] == findStr { arr.append(Range(start:advance(self.startIndex, i),end:advance(self.startIndex, i+findStr.characters.count))) i = i+findStr.characters.count } else { i++ } } return arr } func stringByReplacingOccurrencesOfString(string:String, replacement:String) -> String { // get ranges first using rangesOfString: method, then glue together the string using ranges of existing string and old string let ranges = self.rangesOfString(string) // if the string isn't found return unchanged string if ranges.isEmpty { return self } var newString = "" var startInd = self.startIndex for r in ranges { newString += self[startInd..<r.minElement()!] newString += replacement if r.maxElement() < self.endIndex { startInd = advance(r.maxElement()!,1) } } // add the last part of the string after the final find if (ranges.last!.maxElement()!) < self.endIndex { newString += self[advance(ranges.last!.maxElement()!,1)..<self.endIndex] } return newString } } // try further optimisation by jumping to next index of first search character after every find "a very good hello, hello".stringByReplacingOccurrencesOfString("hello", replacement: "goodbye") // a very good goodbye, goodbye
Thoughts
It is not my ambition to state that these String methods are faster than their Cocoa counterparts, but rather that learning to combine algorithms to replicate Cocoa Framework methods is a first step in becoming comfortable with Swift algorithms.Swift algorithms should be viewed as the ingredients that go into making methods. The fewer ingredients, and the less laborious the method, the better. And once we become used to thinking in algorithms rather than Cocoa Framework methods then we will begin to create our own culinary masterpieces without being confined to built-in recipes.
Nice exploration of the strangeness that is swifts indexes and ranges. I tried that out and it worked, but it was telling me that stringByReplacingOccurrencesOfString was ambiguous, so I changed that name.
ReplyDeleteAlso, in rangesOfString I saw a way to avoid repetition. I changed it to this
func rangesOfString(findStr: String) -> [Range]
{
var result = [Range]()
if !contains(self, first(findStr)!) { // First test whether the string appears at all
return result
}
// set starting point for search based on the finding of the first character
var startInd = find(self, first(findStr)!)!
var i = distance(self.startIndex, startInd)
while i <= countElements(self) - countElements(findStr) {
let range = advance(self.startIndex, i) ..< advance(self.startIndex, i+countElements(findStr))
if self[range] == findStr {
result.append(range)
i = i + countElements(findStr)
}
i++
}
return result
}