Capstone: a records report
Modeling a record as a struct, keeping only the ones that qualify, sorting them, and printing a tidy report.
The shape of the data
A report starts with a record. struct Record with a name, a score and a team gives every row the same three fields, so the rest of the program can rely on them.
A list of those rows is your data set. Building it with a literal keeps this example self-contained: nothing is read from a file or a network, so the output is the same every time.
Each field is a let, which says a record does not change after it is built. The member-wise initialiser takes the fields in the order they are declared, so Record(name:score:team:) must be called with the labels in that order. If a later report needs to edit a score, build a new record instead of mutating the old one; the original list then remains valid for a second summary.
Keep, then order
Two small steps turn the raw list into a report. filter keeps the rows you want, and sorted puts them in order. Each takes a short closure that says what to do with one row.
The sort block compares two rows, written $0 and $1. $0.score > $1.score puts the highest score first, which is the order a reader expects in a ranking.
filter keeps the original order of the rows it accepts, and sorted then reorders them. Swift does not promise that equal elements keep their earlier order, so two rows with the same score may swap between runs. A tie-breaker on the name makes the output repeatable, which matters when a report is compared across runs.
Print it, then summarise
A for loop over the sorted rows prints one line each, using interpolation to place the name, the team and the score where they belong.
A single reduce folds the whole list into a total, and dividing by the count gives the average. Together the lines show the headline number and the row-by-row detail behind it.
The average here is a whole number because total and records.count are both Int. To show a decimal, convert the total before dividing: Double(total) / Double(records.count). If the list could be empty, check for that first; dividing by a count of zero stops the program rather than returning a sensible default.
A pipeline is read from the top down
Each step hands a new collection to the next: filter keeps rows, sorted orders them, and map can turn each row into a line. Nothing is changed in place, so the original list is still available for a second report.
When two rows tie, add a second comparison. $0.score != $1.score ? $0.score > $1.score : $0.name < $1.name breaks the tie by name; without it, equal scores may come back in either order.
A stable tie-breaker is not decoration; it is what makes two runs of the report agree.
A map can turn each record into its printed line, and joined(separator: "\n") can put the lines together with newlines between them. That is the same report as the loop, produced as one string. The loop is easier to read while debugging; the joined string is what you would write to a file or send somewhere else.
What goes wrong
A filter placed after a sort still works, but it wastes the ordering work on rows that are then discarded. Put the cheapest narrowing step first and sort what survives. The same rule applies to a chain of maps: each step runs over the whole intermediate collection.
A sort block that uses >= is not a valid ordering, because two equal rows both claim to come first. The result can vary between runs or even between machines. Use > and add a tie-breaker. And a first on a filtered list returns an optional, so best?.name ?? "none" is the safe way to print it.
The two moves that build a report
struct Record {
let name: String
let score: Int
}
let rows = [
Record(name: "Ada", score: 92),
Record(name: "Alan", score: 78)
]
let top = rows.filter { $0.score >= 80 }
.sorted { $0.score > $1.score }
for row in top {
print(row.name, row.score)
}Three mistakes distort a report. An average built with Int division drops the fraction, so convert to Double when the decimal matters. A sort block must be a strict order: use > rather than >=, or equal rows each claim to come first and the result is undefined. And a dictionary lookup is optional even when the key came from the dictionary’s own keys, so prefer ?? 0 to a forced unwrap.
A ranked score report
Run it and read the ranked lines and the average. Then change the passing mark in the filter and predict how the count and the ranked lines change.
Try these next
- Add a tie-breaker to the ranking so equal scores sort by name.
- Print a team summary that groups rows with a dictionary and sorts the team names.
- Compute the average as a
Doubleand compare it with the whole-number version.
A summary for each team
Run it and check each team’s total and best scorer. Then add a sixth record and predict which team line changes and by how much.
Each piece of this report is a tool you met earlier: a struct from one chapter, the filter and sort from another. Building a report is mostly choosing which of them to line up, in the right order.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why model each row as a struct?
