Capstone: a score report
A program that uses the whole course: records in a struct, an array of them, a sort with your own rule, aligned columns, and a total with one decimal place.
The records and the report
The project is small and complete. Three records hold a name and a score, they go into one array, the array is sorted with the best score first, and the program prints a table, the count, the total, the average to one decimal and the name of the best scorer.
Every piece comes from an earlier chapter: the struct and its literal, the dynamic array, a comparison passed to sort, a format string that lines the columns up, and a loop over the finished list. Nothing here is new, which is the point — the pieces were learned one at a time, and putting them together is the skill this chapter practises.
Sorting by your own rule
sort(rows) orders by the type own comparison, and a struct without one cannot be sorted that way. sort!((a, b) => a.score > b.score)(rows) supplies the rule: it answers whether the first record should come before the second. Writing the greater-than instead of the less-than is what puts the best score first.
The rule has to be consistent. It must never claim that a comes before b and that b comes before a, because the sort is allowed to assume it is being told the truth. Comparing one field is always consistent, which is why it is the rule to reach for first.
The comparison receives two records and answers a bool. It must be a strict order: equal scores compare false in both directions, which is what lets the sort leave them alone. A rule written with >= claims that a record comes before itself, and a sort given that answer may produce an order that changes between runs.
Numbers that line up
writefln("%-5s%5d", r.name, r.score) prints the name left-aligned in five columns and the score right-aligned in five, so the digits stack under each other. A fixed width is the whole trick behind a table that looks deliberate rather than accidental.
The average needs one careful line. Dividing two ints drops the fraction, so cast(double) total moves the division into the fractional world, and %.1f prints exactly one digit after the point. Without the cast the average of 255 and three records would print as 85, and a reader would take the rounded number for the real one.
Data first, presentation last
The program has three stages, and they should stay separate. Building the records is one stage, ordering them is the second, and printing is the third. When a print statement also computes a total, the total is only correct if the loop ran, and the next report that skips the printing loses the number. Compute into variables, then print the variables.
The same split makes the sort testable: the ordering rule is a comparison of two records, not a side effect of the loop that prints them. If the report looks wrong, you can ask whether the data or the presentation is at fault.
Totals that keep the fraction
Summing scores in an int is exact, because every score is a whole number. The average is where the fraction appears, and it appears only if one side of the division is a double. Cast the total once, at the division, rather than casting every score on the way in. For a sum that might pass two billion, use a long for the accumulator.
%.1f rounds to one digit after the point on the way out; it does not change the stored value. A percentage is the same pattern: multiply by 100.0 before dividing, or integer division answers 0 for every fraction below one. Print the number the program computed, not a second, differently rounded one.
The shape of the capstone
import std.stdio;
import std.algorithm;
struct Row {
string name;
int score;
}
void main() {
auto rows = [Row("ada", 88), Row("bob", 95)];
sort!((a, b) => a.score > b.score)(rows);
foreach (r; rows) writeln(r.name, " ", r.score);
}Sort the records and print the report
Run it, then add a fourth record of your own and check that it lands in the right place. Then print the lowest score using rows[$ - 1], and add a line that counts how many records scored 80 or more.
Read the example from top to bottom once before running it. The order is deliberate: the record type, the data, the rule, the sort, then the printing — and most programs you write from here will have roughly that shape.
Sort, total and pass rate
Run it and check that ada comes before dee despite the equal score. Then change ada to 95 and confirm the tie-break still decides the order.
Three mistakes: dividing two ints and printing the truncated average as if it were exact; reversing the comparison so the table reads lowest-first when the sort was meant to put the best first; and a rule that compares two fields inconsistently, so equal scores swap order between runs. Write the tie-break explicitly.
Try these next
- Change ada to 95 and confirm the tie-break still decides who comes first.
- Remove the name comparison and run twice; explain why the order of equal scores is no longer promised.
- Add a column for the gap from the best score and format it in the same width as the score.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What should the function passed to sort! answer?
