Skip to Content
Course content

78: Scanner for Parsing Text

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time using Regular Expressions for parsing text, and while they're powerful, they can quickly become an unreadable mess of backslashes and parentheses. When I need to walk through a string and pull out specific types of data—like a number, then a word, then another number—I reach for Scanner. It's part of Foundation and behaves like a cursor moving across your text, letting you pluck out values as you go.

Let's build something practical: a simple parser for a recipe ingredient list. Imagine we have a string like "2 cups flour, 1.5 tsp salt, 3 eggs" and we want to turn that into a list of structured objects.

Setting up the Scan

First, we need a simple struct to hold our data. I'll keep it basic: a quantity (Double) and a name (String). To use Scanner, we initialize it with the string we want to parse. I prefer using the charactersToBeSkipped property to tell the scanner to ignore whitespace automatically, which saves us from manually skipping spaces between the number and the ingredient name.

struct Ingredient {
    let quantity: Double
    let name: String
}

let recipeText = "2 cups flour, 1.5 tsp salt, 3 eggs"
let scanner = Scanner(string: recipeText)
scanner.charactersToBeSkipped = .whitespacesAndNewlines

Extracting the Quantities

The beauty of Scanner is the type-specific methods. Instead of slicing the string and trying to cast it to a Double, I can just call scanDouble(). If the scanner finds a number at its current position, it consumes it and returns it; otherwise, it returns nil.

var ingredients: [Ingredient] = []

while !scanner.isAtEnd {
    if let quantity = scanner.scanDouble() {
        // Now we need the name...
    }
}

The "Stop Character" Trap

Here is where I usually trip up the first time I use this. I need to get the name of the ingredient, but the ingredients are separated by commas. I'll use scanUpTo, which reads everything until it hits a specific character. Let's see what happens when I try to grab the name.

// My first attempt (which had a bug)
if let quantity = scanner.scanDouble() {
    if let name = scanner.scanUpTo(", ") {
        ingredients.append(Ingredient(quantity: quantity, name: name))
    }
}

I ran this and noticed something annoying: the first ingredient was "cups flour", but the second one was ", tsp salt". Why? Because scanUpTo stops at the character you specify, but it doesn't consume it. The comma is still sitting there at the front of the cursor, so the next time the loop runs, scanDouble() fails immediately because it sees a comma, not a number.

To fix this, I have to manually tell the scanner to skip that comma if it's there. I'll use scanString to move the cursor past the delimiter.

Finalizing the Parser Loop

Now that I've accounted for the trailing comma, the logic holds up. I'll wrap it in a loop that continues until the cursor reaches the end of the string. I'm adding a small check to make sure we don't get stuck in an infinite loop if the scanner encounters a character it doesn't expect.

var ingredients: [Ingredient] = []
let scanner = Scanner(string: recipeText)
scanner.charactersToBeSkipped = .whitespacesAndNewlines

while !scanner.isAtEnd {
    if let quantity = scanner.scanDouble() {
        // Scan everything until the comma
        if let name = scanner.scanUpTo(",") {
            ingredients.append(Ingredient(quantity: quantity, name: name))
        }
        
        // IMPORTANT: Consume the comma so we can move to the next item
        _ = scanner.scanString(",")
    } else {
        // If we can't find a number, just move forward one char to avoid infinite loop
        _ = scanner.scanCharacter()
    }
}

for item in ingredients {
    print("Found: \(item.quantity) of \(item.name)")
}

This approach is much cleaner than splitting the string into an array and then splitting those arrays again. You're treating the string as a stream, which is exactly how parsing should feel.




📋 Practical Task

Build a Game Map Coordinate Parser

In many old-school game save files, coordinates are stored as a single string of pairs, such as: "X:10 Y:20 X:15 Y:30 X:40 Y:10".

Your task is to create a Coordinate struct (with x and y as Ints) and use a Scanner to parse the string above into an array of Coordinate objects.

Requirements:

  • Use scanString("X:") and scanString("Y:") to navigate the labels.
  • Use scanInt() to extract the numeric values.
  • Ensure the scanner correctly consumes all parts of the string so that you end up with an array of 3 Coordinate objects.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.