Extending Swift's String Class

2016-01-24

The API for Swift's String class feels incomplete, especially after working with a language like Ruby, where manipulating strings is especially easy. Granted Swift is a young language and will certainly improve with time.

What's nice about Swift right now, though, is that it provides its users with a simple way to extend the base language. Let's look at implementing a basic version of Ruby's gsub in Swift.

First, it's worth noting that Swift currently has a few methods which come close to Ruby's gsub, namely:

func stringByReplacingOccurrencesOfString(
  _ target: String,
  withString replacement: String) -> String

func stringByReplacingCharactersInRange(
  _ range: NSRange,
  withString replacement: String) -> String

These methods work well for replacing a known substring, but otherwise do not support regular expressions. To use regular expressions in Swift, one must use NSRegularExpression. To replace all matches of a particular regular expression, the NSRegularExpression class includes:

func stringByReplacingMatchesInString(
  _ string: String,
  options options: NSMatchingOptions,
  range range: NSRange,
  withTemplate templ: String) -> String

For example, assuming we have already created a NSRegularExpression object, invoking the aforementioned method looks something like this:

let myString = "some string with numbers 12345"

someNonDigitsRegexp.stringByReplacingMatchesInString(
  myString,
  options: [], // omitting all NSMatchingOptions
  range: NSMakeRange(0, myString.characters.count),
  withTemplate: "" // replace all letters and spaces with nothing
)

While the method above works perfectly well, it's also verbose and a handful for a simple replacement using regular expressions. To add to the verbose method call, creating an NSRegularExpression object isn't so straight forward either.

Suppose we want to create a regular expression which matches anything which isn't a digit. We might try the following:

NSRegularExpression(pattern: "[^\\d]", options: [])

The problem with the expression above is that the constructor for NSRegularExpression can throw an error, and so we must be prepared for the constructor to fail. Now our code becomes:

let myString = "some string with numbers 12345"

if let someNonDigitsRegexp = try? NSRegularExpression(pattern: "[^\\d]", options: []) {
  someNonDigitsRegexp.stringByReplacingMatchesInString(
    myString,
    options: [], // NSMatchingOptions
    range: NSMakeRange(0, myString.characters.count),
    withTemplate: "" // replace all letters and spaces with nothing
  )
}

By prefacing the NSRegularExpression constructor with a try?, we handle the case where the constructor throws and at the same time we convert the result into an optional. If the constructor succeeds, we have our regular expression. If the constructor fails, we have nil. Finally, by using the if let syntax, we have a concise bit of code which will proceed with using the regular expression if the constructor succeeds. Otherwise, we skip the body of the if block when the constructor returns nil.

Altogether, though, the code above is verbose and bursting with unnecessary information. In this case, we don't care about the options argument passed to the NSRegularExpression constructor. We also don't care about the range or options arguments in stringByReplacingMatchesInString method. So let's wrap this up in an extention to the String class to hide all these unnecessary details:

extension String {
  func replaceMatches(regexp: String, with replacement: String) -> String {
    if let regex = try? NSRegularExpression(pattern: regexp, options: []) {
      return regex.stringByReplacingMatchesInString(
        self,
        options: [],
        range: NSMakeRange(0, self.characters.count),
        withTemplate: replacement
      )
    }

    return self
  }
}

With our extension above, we can now do the following:

let myString = "some string with numbers 12345"

myString.replaceMatches("[^\\d]", with: "") // "12345"

The above code is much cleaner. Granted, if the NSRegularExpression constructor fails, we're simply returing the original string. Provided we have good tests around the code which uses the extension, I think it's a reasonable trade-off.

Finally, it's not clear to me how extensions should be named, e.g., prefixing the method name with some unique initials to prevent collisions. Nonetheless, I greatly appreciate the way Swift allows its users to extend the language.