Перейти к содержимому
Теория · Итоговый проект · урок 2 из 5

Итоги по категориям

Чеки лежат в тетради, и пора подвести итоги: сколько ушло на еду, сколько на такси и сколько всего. На бумаге вы берёте закладку food, складываете чеки под ней и пишете сумму на закладке. Потом то же самое с taxi, а внизу страницы — общий итог sum.

В программе чеки лежат одной стопкой, поэтому считать удобнее иначе. Программа проходит по стопке один раз и прибавляет каждый чек к итогу его категории. Итоги хранит словарь: ключ — категория, значение — сумма по ней.

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

Давайте разберём программу по шагам:

  1. Первые семь строк — модель тетради без description: печатать чеки целиком здесь не нужно.
  2. var totals: [Category: Double] = [:] — пустой словарь итогов. Тип указан явно: по одному [:] Swift его не выведет.
  3. Цикл берёт чеки по одному, и строка внутри него прибавляет стоимость чека к итогу его категории.
  4. Последняя строка читает итоги с запасным нулём и печатает 16.75 на еду и 7.0 на такси.

Итог чек за чеком

Главная строка программы — totals[e.category, default: 0] += e.cost. Она ищет итог категории чека, а если ключа ещё нет, берёт 0. Потом прибавляет стоимость и записывает результат под тем же ключом. Так первый чек категории создаёт пару в словаре, а каждый следующий её увеличивает.

Ключом словаря может быть только значение Hashable-типа. Swift сам делает перечисление без связанных значений Hashable, поэтому Category годится в ключи без всяких пометок.

Нажимайте «Следующий шаг» и следите за словарём totals после строки 15: пара .food появляется на первом чеке еды, а на втором только растёт. Виджет записывает пары в порядке появления, а настоящий словарь порядка не хранит.

ВживуюИтоги растут чек за чекомшаг 1 из 11 · рамок: 1

Словарь итогов пуст: ни одной пары в нём ещё нет.

Следите за словарём totals после строки 15

Что видно в виджете:

  • default: 0 срабатывает дважды: на первом чеке еды и на первом чеке такси;
  • третий чек не создаёт новую пару, а прибавляет 4.25 к 12.5;
  • пар в словаре столько, сколько разных категорий, а не сколько чеков.

Сгруппировать записи по ключу и посчитать сумму в каждой группе — частая задача на собеседовании. Решают её так же: словарь итогов и один проход с default:.

Общая сумма: reduce

Итог внизу страницы — сумма всех чеков. Её собирает reduce:

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

reduce(0) начинает с нуля. В замыкании $0 — накопленная сумма, а $1 — очередной чек, и к сумме прибавляется его cost. Сложение идёт так:

  1. 0 + 12.5 даёт 12.5.
  2. 12.5 + 7.0 даёт 19.5.
  3. 19.5 + 4.25 даёт 23.75 — это и есть результат.

Печать по порядку

Словарь не хранит порядок пар. Если печатать итоги циклом for (category, total) in totals, при одном запуске первой выйдет еда, при другом — такси. А тетрадь должна выглядеть одинаково при каждом запуске. Поэтому итоги печатают по Category.allCases: варианты в нём всегда идут в порядке объявления.

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]

Первые три строки вывода этого блока — страница тетради:

  • food 16.75
  • taxi 7.0
  • sum 23.75

Последняя строка блока — короткий путь, если нужны только числа. map идёт по тем же allCases и собирает итоги в массив: сначала еда, потом такси.

Здесь картинка с тетрадью неточна. Закладка в тетради есть, даже если под ней пусто. А пара в словаре totals появляется только с первым чеком категории: без поездок на такси не было бы и ключа .taxi. Поэтому итоги читают через default: 0, и пустая категория выходит с итогом 0.0.

Частая ошибка

Ключи отсортировали, как в теме о словарях:

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'

Программа не компилируется. sorted() сортирует только значения Comparable-типа: строки и числа это умеют, а Category — нет. Сообщение длинное, но суть в конце: Category должна соответствовать Comparable. Печатайте по Category.allCases: так короче, и категория без чеков не пропадёт.

Главное

  • Итоги по категориям — словарь [Category: Double]; его заполняет один проход по чекам: totals[e.category, default: 0] += e.cost.
  • Swift сам делает перечисление без связанных значений Hashable, поэтому оно годится в ключи словаря.
  • Общую сумму собирает reduce(0) { $0 + $1.cost }.
  • Итоги печатают по Category.allCases: порядок объявления не меняется от запуска к запуску, а пустая категория выходит с 0.0.