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.

Endorse on Coderwall

Comments

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

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

    ReplyDelete

Post a Comment