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.