Skip to content
Theory · Final project · lesson 2 of 5

Totals by category

The receipts are in the ledger, and it's time to add things up: how much went on food, how much on taxis and how much in total. On paper you take the food tab, add up the receipts under it and write the amount on the tab. Then the same with taxi, and at the bottom of the page the grand total, sum.

In the program the receipts sit in one stack, so it is easier to count differently. The program goes through the stack once and adds each receipt to its category's total. A dictionary keeps the totals: the key is a category, the value is the amount for it.

SWIFT
enum Category: String, CaseIterable {
    case food, taxi
}
struct Expense {
    let category: Category
    let cost: Double
}
let expenses = [Expense(category: .food, cost: 12.5),
                Expense(category: .taxi, cost: 7),
                Expense(category: .food, cost: 4.25)]
var totals: [Category: Double] = [:]
for e in expenses {
    totals[e.category, default: 0] += e.cost
}
print(totals[.food, default: 0], totals[.taxi, default: 0]) // 16.75 7.0

Let's take the program step by step:

  1. The first seven lines are the ledger's model without description: there is no need to print whole receipts here.
  2. var totals: [Category: Double] = [:] is an empty dictionary of totals. The type is written out: from [:] alone Swift cannot infer it.
  3. The loop takes the receipts one by one, and the line inside it adds the receipt's cost to its category's total.
  4. The last line reads the totals with a fallback zero and prints 16.75 for food and 7.0 for taxis.

The total, receipt by receipt

The key line of the program is totals[e.category, default: 0] += e.cost. It looks up the total for the receipt's category, and if the key isn't there yet, it takes 0. Then it adds the cost and writes the result under the same key. So the first receipt of a category creates a pair in the dictionary, and each later one increases it.

A dictionary key can only be a value of a Hashable type. Swift makes an enum without associated values Hashable by itself, so Category works as a key without any marking.

Press "Next step" and watch the totals dictionary after line 15: the .food pair appears on the first food receipt and only grows on the second. The widget writes the pairs in the order they appear, while a real dictionary keeps no order.

LiveTotals grow receipt by receiptstep 1 of 11 · frames: 1

The totals dictionary is empty: it has no pairs yet.

Watch the totals dictionary after line 15

What the widget shows:

  • default: 0 kicks in twice: on the first food receipt and on the first taxi receipt;
  • the third receipt creates no new pair but adds 4.25 to 12.5;
  • the dictionary has as many pairs as there are different categories, not as many as there are receipts.

Grouping records by a key and adding up each group is a common interview task. It is solved the same way: a dictionary of totals and one pass with default:.

The grand total: reduce

The total at the bottom of the page is the sum of all receipts. reduce builds it:

SWIFT
let sum = expenses.reduce(0) { $0 + $1.cost }
print(sum) // 23.75

reduce(0) starts from zero. In the closure $0 is the running sum and $1 is the next receipt, and its cost is added to the sum. The addition goes like this:

  1. 0 + 12.5 gives 12.5.
  2. 12.5 + 7.0 gives 19.5.
  3. 19.5 + 4.25 gives 23.75, which is the result.

Printing in order

A dictionary keeps no order of its pairs. If you print the totals with the loop for (category, total) in totals, one run puts food first and another puts taxis first. But the ledger should look the same on every run. So the totals are printed by Category.allCases: its cases always come in declaration order.

SWIFT
for category in Category.allCases {
    print(category.rawValue, totals[category, default: 0])
}
print("sum", sum) // sum 23.75
print(Category.allCases.map { totals[$0, default: 0] }) // [16.75, 7.0]

The first three lines this block prints are a page of the ledger:

  • food 16.75
  • taxi 7.0
  • sum 23.75

The block's last line is a shortcut for when you need only the numbers. map walks the same allCases and collects the totals into an array: food first, then taxis.

Here the ledger picture is inaccurate. A tab in the ledger is there even when nothing lies under it. But a pair in the totals dictionary appears only with the category's first receipt: with no taxi rides there would be no .taxi key at all. That's why the totals are read with default: 0, and an empty category comes out with the total 0.0.

Common mistake

The keys were sorted, as in the theme on dictionaries:

SWIFT
for category in totals.keys.sorted() { // ✗
    print(category.rawValue, totals[category, default: 0])
}
TEXT
error: referencing instance method 'sorted()' on 'Sequence' requires that 'Dictionary<Category, Double>.Keys.Element' (aka 'Category') conform to 'Comparable'

The program does not compile. sorted() sorts only values of a Comparable type: strings and numbers can do it, and Category cannot. The message is long, but the point is at the end: Category has to conform to Comparable. Print by Category.allCases: it is shorter, and a category with no receipts doesn't get lost.

Key points

  • The totals by category are a [Category: Double] dictionary filled by one pass over the receipts: totals[e.category, default: 0] += e.cost.
  • Swift makes an enum without associated values Hashable by itself, so it works as a dictionary key.
  • reduce(0) { $0 + $1.cost } builds the grand total.
  • The totals are printed by Category.allCases: declaration order does not change from run to run, and an empty category comes out with 0.0.