AppDeveloperKit-String: A Swift library for subscripting, regular expression matching and substitutions

String shortcomings

I really enjoy programming in Swift, but I’ve always been bothered a bit by what I perceived as a couple of shortcomings related to the String class.

Subscripting

There is no support for subscripting a String.

Try the following in a Playground and you’ll get an error:

 let str = "Hello"

 str[1]   // 'subscript' is unavailable: cannot subscript String with an Int
 str[1..<3] // 'subscript' is unavailable: cannot subscript String with an integer range

You can find a mention of this restriction in some source code comments.

I think this restriction is unnecessary.   A String.Index can be used to get the  position of a character or code unit in a string.   Using the method index(_ i, offsetBy distance:) one can get references to Unicode characters including those containing ZWJ sequences (which this article discusses).

Here is a nice answer on Stack Overflow that discusses this possibility.

Matching and Substitutions

Suppose that you want to perform some matching on a String and extract some portions using a regular expression.    The following illustrates that this is not a simple exercise and involves a  fair amount of code.


// Form a regular expression.  It can throw!
var regex: NSRegularExpression
do {      
    regex = try NSRegularExpression(pattern: pattern, options: [])         
}
catch {
    <handle errors>
} 

// Get the matches
let inputStrRange = inputStr.startIndex..<inputStr.endIndex
let inputStrRange_ns = NSRange(inputStrRange, in: inputStr)
let matches = regex.matches(in: inputStr, options: [], range: inputStrRange_ns)

for matchesIndex in 0..<matches.count  {
            
    let numberOfRanges = matches[matchesIndex].numberOfRanges
            
    for rangeIndex in 0..<numberOfRanges {
        let range = matches[matchesIndex].range(at: rangeIndex)  // An NSRange!

        if range.location == NSNotFound {
            <handle situation where a capture group is not part of a match>
        }

        // Now we can extract our String, but we need a Range not an NSRange
        guard let matchRange =  Range(range, in: inputStr) else {
            <handle nil>
        }

        let match = inputStr[matchRange.lowerBound..<matchRange.upperBound]
        
        // And so on ...


 

Introducing AppDeveloperKit-String

I created AppDeveloperKit-String to make subscripting, matching and substitutions easier.   For matching and substitutions, I decided to use a form that was inspired by Perl.

Subscripting

Subscripting now works as expected:

 let str = "Hello"

 str[1]     // "e"
 str[1..<3] // "el"

Matching

Matching is now simple.


// Using the =~ operator.  Shows use of the case insensitive flag.
 var str = "4XY5"
 var (a,b) = str =~ (.M, "([a-z])([a-z])", "i", { (preMatch, match, postMatch) in  // ("X", "Y")
    preMatch // 4
    match    // XY
    postMatch // 5
 })

// With a more conventional method call (also part of AppDeveloperKit-String)
 (a,b) = Regex.m(str: str, pattern: "([a-z])([a-z])", flags: "i", completion: { (preMatch, match, postMatch) in // ("X","Y")
    preMatch // 4
    match    // XY
    postMatch // 5
 })


// Array output is supported as is the global flag.  The completion arg is not required.
var arr: [String] = str =~ (.M, "([a-z])", "ig") // ["X", "Y"]


// The flags argument is also not required.
str = "XY"
arr = str =~ (.M, "(\\w)") // ["X"]

Substitutions


// A template is supported in replacement string.
var str = "XY ZW"
count = str =~ (.S, "(\\w)(\\w)","$2$1", "g") // 2
str // YX WZ

Many more features

There are many more features and examples.  Check out the main page of AppDeveloperKit-String     For an even more comprehensive specification, take  look at the Documentation which is provided in the form of a Swift Playground or PDF file.

A test driven approach

I decided that I was going to spend a lot of time upfront developing the test infrastructure.    As I was researching how I wanted the library to behave, I would add test cases to cover expected behavior – often before the associated library code was even implemented.

I also enabled code coverage to ensure that I was exercising all the library code.

I found and fixed several bugs due to the set of unit tests that I implemented.  In particular my handling of capture groups (some may not participate in a match) and Unicode characters (including the special case of ZWJ sequences) benefited from a robust set of tests.

Along the way I learned some additional Swift tricks, including the use of the Mirror structure to convert a tuple into an Array.   The following function from my test code shows how this is done:

 


    // Array from tuple
    //
    // Reference:
    // https://appventure.me/2015/07/19/tuples-swift-advanced-usage-best-practices/
    //
    class func arrayFromTuple(tuple: Any?) -> [String?] {
        
        // Handle single value tuple that has a nil element.  Comes through as tuple == nil
        if tuple == nil {
            return [nil]
        }
        
        let mirror = Mirror(reflecting: tuple ?? [nil])
        var result: [String?] = []
        
        // Handle single value tuple that has a non-nil element. Comes through as tuple == string value
        if mirror.children.count == 0 {
            if let str = tuple as? String {
                return [str]
            }
            else {
                assertionFailure("Expecting only tuples containing String?")
                return [nil]
            }
        }
        
        for (_, value) in mirror.children {
            result.append(value as? String)
        }
        
        return result
    }
    
}


Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s