Java from the ground up

Putting it together: a small report

The whole course in one file: a class for each record, a HashMap to hold them, a sort, and a printed report that anyone can read.

Part 12 of 12

Advanced20 min readBack to Courses

The shape of a small program

Twelve chapters of pieces, arranged into one file. The data is a class: a Sale carries the name of the item and its price in whole cents. The map holds the sales under their item names, the list brings them out, and the loop prints them in order.

The pattern is worth remembering as three moves. Values live in a class, questions are asked with collections and loops, and the output is built at the very end. Almost every confusing program is missing one of those separations, and almost every clear one has all three.

Why the price is stored in cents

Prices here are whole numbers of cents rather than doubles. Money and fractions do not mix well: a double cannot hold 0.10 exactly, so sums drift by a fraction of a cent and a printed total stops matching the arithmetic. Whole cents keep the arithmetic exact, and the formatting happens only when a person reads it.

This is why the report prints 520 and not 5.20. Turning cents into pounds or dollars at the edge, in one place, is the habit that keeps money honest.

A map has no order, so sort the values

A HashMap promises nothing about the order of its keys, so nothing is printed straight from it. new ArrayList<>(sales.values()) copies the sales into a list, and rows.sort(...) puts the list in order — largest price first, because the comparator subtracts the second price from the first.

That comparator is the one new piece of syntax: (a, b) -> b.cents - a.cents answers with a negative number when a should come first. Swapping the two halves sorts the other way round, which is the smallest change worth trying on your own.

From working code to a readable report

The capstone already has the three moves: a class for the data, collections for the questions, and output at the end. The next step is to make the output worth reading. A total, a count and an average turn a list of rows into a summary, and each one is a small loop or a method away.

Keep the summary separate from the printing. A method that returns the total can be tested and reused; a method that prints the total can only be read. The same split applies to formatting: compute in whole numbers, format at the edge.

Comparators, ties and averages

A comparator answers negative, zero or positive: negative means the first argument comes first, zero means they tie, positive means the second comes first. Integer.compare(a.cents, b.cents) says that without arithmetic, and it cannot overflow the way a.cents - b.cents can. Comparator.comparing(row -> row.cents) builds the same comparison from a key, and reversed() flips it, which is shorter to read when the key is a single field.

When two rows tie, a second comparison decides the order. Chain them: compare the price, and if the answer is zero, compare the name. A report that changes order between runs is hard to read even when every value is correct.

Pence, pounds and the last division

Whole cents make the arithmetic exact, and they also make the printing a small problem of its own. cents / 100 gives the whole pounds and drops the remainder; cents % 100 gives the pence. Printed separately, 520 becomes 5 and 20, and the second number needs a leading zero when it is under ten. System.out.printf("%d.%02d%n", cents / 100, cents % 100) does both in one line, and the 02 is what keeps 5.05 from printing as 5.5.

The same split works for any unit with a hundred parts, and it is worth writing once as a method. A money(int cents) that returns the text keeps the format in one place, so a change from pounds to euros touches one line instead of every print statement.

The same shape over different data

class Reading {
    final String place;
    final int degrees;

    Reading(String place, int degrees) {
        this.place = place;
        this.degrees = degrees;
    }
}

Map<String, Reading> byPlace = new HashMap<>();
byPlace.put("oslo", new Reading("oslo", 4));
byPlace.put("cairo", new Reading("cairo", 31));

List<Reading> rows = new ArrayList<>(byPlace.values());
rows.sort((a, b) -> b.degrees - a.degrees);

for (Reading row : rows) {
    System.out.println(row.place + " " + row.degrees);
}
System.out.println("readings " + rows.size());

Three sales, sorted and printed

Press Run and read the report. Then add a total: declare a counter before the loop, add each price inside it, and print it after the last line. Finally, try sorting smallest first by swapping the two prices in the comparator.

Output
Click "Run" to execute your code

Three report mistakes: dividing two ints and expecting a decimal average, subtracting two large prices in a comparator where the result can overflow, and modifying the map while a loop is walking its values. Cast one side of the division to double, use Integer.compare, and copy the values into a list before sorting.

Try these next

  • Add a total and a count to the existing report, then print the average price with two decimal places.
  • Change the comparator to sort by name when two prices are equal, and add a tie to the data to see it work.
  • Replace the Sale class with a second class of your own and keep the map, the sort and the loop unchanged.

A category report with averages

Press Run and check each category total and average against the four items. Then add a third drink and a second food item, and predict which averages move before you run it.

Output
Click "Run" to execute your code

The same file with a different class becomes a different report: the map, the sort and the loop do not change. That is the reward for putting the data in a class and the printing at the end.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Why does the report store prices as whole cents?