Parsing input and errors
So far the receipts reached the ledger straight from the code. In real life they are entered as text: for example, you jot your spending into a notes app on your phone, one line per purchase: food 12.5, taxi 7. The program has to turn each such line into a receipt.
But lines can contain mistakes. fod 3 has a typo in the category, taxi abc has letters instead of a cost, and food forgot the cost. A careful bookkeeper doesn't guess what was meant. They set such a receipt aside with a note giving the reason and enter the rest into the ledger.
Parsing is turning text into the program's values: here, lines into receipts. Let's start with one correct line:
enum Category: String, CaseIterable {
case food, taxi
}
struct Expense {
let category: Category
let cost: Double
}
let parts = "taxi 7".split(separator: " ")
let category = Category(rawValue: String(parts[0]))
let cost = Double(parts[1])
if let category, let cost {
let ride = Expense(category: category, cost: cost)
print(ride.category.rawValue, ride.cost) // taxi 7.0
}Let's take the program step by step:
split(separator: " ")cuts the line at the space into two parts:taxiand7.- The parts have the type
Substring: they are pieces of the original string.Category(rawValue:)expects exactly aString, so the piece is wrapped inString(…). Double(parts[1])reads the number, and it accepts the piece as it is.- Both the category and the cost are optionals.
if let category, let costopens both boxes, and the program printstaxi 7.0.
What can go wrong
A line can be wrong in three ways:
fod 3: there is nofodtab, andCategory(rawValue:)givesnil;taxi abc:abcholds no number, andDoublegivesnil; the same happens with12,5: Swift expects a point, not a comma;food: there is only one part, andparts[1]crashes the program.
Fatal error: Index out of rangeWith if let, the first two lines silently fail the check, and the person never learns why. And the third one stops the whole program. We need a function that checks a line step by step and names the reason it was rejected. Swift has errors for this.
The parse function
Let's describe the three reasons with an enum and throw the right one when a check fails:
enum InputError: Error {
case badFormat, badCategory, badCost
}
func parse(_ line: String) throws -> Expense {
let parts = line.split(separator: " ")
guard parts.count == 2 else { throw InputError.badFormat }
guard let category = Category(rawValue: String(parts[0])) else {
throw InputError.badCategory
}
guard let cost = Double(parts[1]) else { throw InputError.badCost }
return Expense(category: category, cost: cost)
}Let's take the function apart:
InputErrornames the reasons: not two parts, an unknown category, a cost that is not a number.- The first
guardchecks the number of parts. It comes beforeparts[1], so the linefoodno longer crashes the program. - The second
guardlooks up the tab by its label, and the third reads the cost. - When every check passes,
parsereturns a ready receipt.
The constants parts, category and cost inside the function are its own. They have nothing to do with the constants of the same names in the first block.
Bad lines in catch
Now let's read a whole list of lines. Each line is parsed in its own do, so a mistake in one line doesn't get in the way of the others:
let lines = ["food 12.5", "taxi abc", "fod 3", "taxi 7", "food", "food 4.25"]
var expenses: [Expense] = []
for line in lines {
do {
expenses.append(try parse(line))
} catch {
print("skip \"\(line)\":", error)
}
}
let sum = expenses.reduce(0) { $0 + $1.cost }
print(expenses.count, "sum", sum) // 3 sum 23.75This block prints four lines:
skip "taxi abc": badCostskip "fod 3": badCategoryskip "food": badFormat3 sum 23.75
Let's see what happened:
- The
do/catchsits inside the loop, so the error of one line is caught, and the loop moves on to the next one. - In
catchthe caught error is in the constanterror, andprintshows its case, the reason the line was rejected. - The three good lines became receipts for
12.5,7.0and4.25, and together they make23.75.
Interviewers like to ask when to catch an error with do/catch and when try? is enough. The answer: try? turns an error into nil, and lines.compactMap { try? parse($0) } gives the same three receipts in one line. But together with the bad lines it discards the reasons too. If the person needs to know what to fix, catch the error in catch.
Here the bookkeeper picture is inaccurate. A bookkeeper also notices an odd receipt, such as a lunch for a million. The program catches only what parse checks, and it accepts the line food 1000000 without a question.
Common mistake
A piece of the line was passed to Category(rawValue:) without String(…):
let tab = Category(rawValue: parts[0]) // ✗error: cannot convert value of type 'String.SubSequence' (aka 'Substring') to expected argument type 'String'The program does not compile. split returns pieces of type Substring, while Category(rawValue:) accepts only a String. This mistake is common because Double(parts[1]) does accept the same piece. Wrap the piece: Category(rawValue: String(parts[0])).
Key points
split(separator: " ")cuts a line into pieces of typeSubstring; forCategory(rawValue:)a piece is wrapped inString(…), whileDouble(…)accepts it as it is.parsechecks a line step by step withguardand throws its ownInputErrorcase for each reason; the number of parts is checked beforeparts[1].- A
do/catchinside the loop catches the error of one line, names the reason and lets the loop go on. compactMap { try? parse($0) }is shorter, but it silently discards the bad lines together with the reasons.