Blog

Swift Tip: An NSScanner Alternative

For one reason or another, we find ourselves writing small scanners and parsers quite often. Sometimes we parse a specific file format, or a small expression language, or just a file name that conforms to a certain naming scheme.

One approach is to use the Scanner class from Foundation (it used to be called NSScanner ). A Scanner instance stores the scanned String and a scan location, which is the position in the string. For example, scanning a single character just returns the character at the current scan location and increases the scan location.

In pure Swift, there's another type that stores a String and an offset into that String : Substring . Instead of using a scanner, we could write mutating methods on Substring . As an illustration, here are three such methods. The first matches a character that matches a certain condition, the second scans exactly count characters, and the last scans a specific prefix:

								extension Substring {
    mutating func scan(_ condition: (Element) -> Bool) -> Element? {
        guard let f = first, condition(f) else { return nil }
        return removeFirst()
    }

    mutating func scan(count: Int) -> Substring? {
        let result = prefix(count)
        guard result.count == count else { return nil }
        removeFirst(count)
        return result
    }

    mutating func scan<C>(prefix: C) -> Bool where C: Collection, C.Element == Character {
        guard starts(with: prefix) else { return false }
        removeFirst(prefix.count)
        return true
    }
}

							

To use this with strings, we first need to make a mutable Substring out of a String , and then we can call the scan method:

								var remainder = "value: 123"[...]
if remainder.scan(prefix: "value: "),
   let firstDigit = remainder.scan({ "0123456789".contains($0) }) {
  print(firstDigit)
}

							

You can write a whole bunch of these scanning methods, there is no need for an extra Scanner type. You can even write "higher-order" scanners, like this:

								extension Substring {
  mutating func many<A>(until end: Character, _ f: (inout Substring) throws -> A, separator: (inout Substring) throws -> Bool) throws -> [A] {
    // ... left as an exercise
  }
}

							

So far, we could have done similar things with a Scanner . However, one of the fun things about Swift is that the code we write is actually far more generic! Instead of defining it on Substring , we can define it on any Collection that supports removeFirst . Reviewing the method's definition , we learn that it exists on any collection that has itself as a Subsequence . This means we only have to change the definition of the method, but not the method body:

								extension Collection where SubSequence == Self {
    mutating func scan(_ condition: (Element) -> Bool) -> Element? {
        guard let f = first, condition(f) else { return nil }
        return removeFirst()
    }

    mutating func scan(count: Int) -> Self? {
        let result = prefix(count)
        guard result.count == count else { return nil }
        removeFirst(count)
        return result
    }
}

extension Collection where SubSequence == Self, Element: Equatable {
    mutating func scan<C>(prefix: C) -> Bool where C: Collection, C.Element == Element {
        guard starts(with: prefix) else { return false }
        removeFirst(prefix.count)
        return true
    }
}

							

Now we can use our scan method on many other types as well, most notably ArraySlice and Data . For example, we can use it to parse the beginning of a GIF header:

								var t = try! Data(contentsOf: URL(string: "https://media.giphy.com/media/gw3IWyGkC0rsazTi/giphy.gif")!)[...]
guard t.scan(prefix: [71,73,70]), // GIF
    let version = t.scan(count: 3), // 87a or 89a
    let width = t.scan(count: 2),
	let height = t.scan(count: 2)
    else {
    fatalError()
}

print(version, width, height)

							

For further inspiration, see this gist by Michael Ilseman .

In Swift Talk Episode 78 (a public episode), we show how to work with Swift's String and Substring types by writing a simple CSV parser.

To support our work you can subscribe , or give someone a gift .


Stay up-to-date with our newsletter or follow us on Twitter .

Back to the Blog

Recent Posts