Итоги по категориям
Чеки лежат в тетради, и пора подвести итоги: сколько ушло на еду, сколько на такси и сколько всего. На бумаге вы берёте закладку food, складываете чеки под ней и пишете сумму на закладке. Потом то же самое с taxi, а внизу страницы — общий итог sum.
В программе чеки лежат одной стопкой, поэтому считать удобнее иначе. Программа проходит по стопке один раз и прибавляет каждый чек к итогу его категории. Итоги хранит словарь: ключ — категория, значение — сумма по ней.
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Давайте разберём программу по шагам:
- Первые семь строк — модель тетради без
description: печатать чеки целиком здесь не нужно. var totals: [Category: Double] = [:]— пустой словарь итогов. Тип указан явно: по одному[:]Swift его не выведет.- Цикл берёт чеки по одному, и строка внутри него прибавляет стоимость чека к итогу его категории.
- Последняя строка читает итоги с запасным нулём и печатает
16.75на еду и7.0на такси.
Итог чек за чеком
Главная строка программы — totals[e.category, default: 0] += e.cost. Она ищет итог категории чека, а если ключа ещё нет, берёт 0. Потом прибавляет стоимость и записывает результат под тем же ключом. Так первый чек категории создаёт пару в словаре, а каждый следующий её увеличивает.
Ключом словаря может быть только значение Hashable-типа. Swift сам делает перечисление без связанных значений Hashable, поэтому Category годится в ключи без всяких пометок.
Нажимайте «Следующий шаг» и следите за словарём totals после строки 15: пара .food появляется на первом чеке еды, а на втором только растёт. Виджет записывает пары в порядке появления, а настоящий словарь порядка не хранит.
1enum Category: String, CaseIterable {2 case food, taxi3}4struct Expense {5 let category: Category6 let cost: Double7}8let expenses = [9 Expense(category: .food, cost: 12.5),10 Expense(category: .taxi, cost: 7),11 Expense(category: .food, cost: 4.25),12]13var totals: [Category: Double] = [:]14for e in expenses {15 totals[e.category, default: 0] += e.cost16}17for c in Category.allCases {18 print(c.rawValue, totals[c, default: 0])19}
Словарь итогов пуст: ни одной пары в нём ещё нет.
Что видно в виджете:
default: 0срабатывает дважды: на первом чеке еды и на первом чеке такси;- третий чек не создаёт новую пару, а прибавляет
4.25к12.5; - пар в словаре столько, сколько разных категорий, а не сколько чеков.
Сгруппировать записи по ключу и посчитать сумму в каждой группе — частая задача на собеседовании. Решают её так же: словарь итогов и один проход с default:.
Общая сумма: reduce
Итог внизу страницы — сумма всех чеков. Её собирает reduce:
let sum = expenses.reduce(0) { $0 + $1.cost }
print(sum) // 23.75reduce(0) начинает с нуля. В замыкании $0 — накопленная сумма, а $1 — очередной чек, и к сумме прибавляется его cost. Сложение идёт так:
0 + 12.5даёт12.5.12.5 + 7.0даёт19.5.19.5 + 4.25даёт23.75— это и есть результат.
Печать по порядку
Словарь не хранит порядок пар. Если печатать итоги циклом for (category, total) in totals, при одном запуске первой выйдет еда, при другом — такси. А тетрадь должна выглядеть одинаково при каждом запуске. Поэтому итоги печатают по Category.allCases: варианты в нём всегда идут в порядке объявления.
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.75taxi 7.0sum 23.75
Последняя строка блока — короткий путь, если нужны только числа. map идёт по тем же allCases и собирает итоги в массив: сначала еда, потом такси.
Здесь картинка с тетрадью неточна. Закладка в тетради есть, даже если под ней пусто. А пара в словаре totals появляется только с первым чеком категории: без поездок на такси не было бы и ключа .taxi. Поэтому итоги читают через default: 0, и пустая категория выходит с итогом 0.0.
Частая ошибка
Ключи отсортировали, как в теме о словарях:
for category in totals.keys.sorted() { // ✗
print(category.rawValue, totals[category, default: 0])
}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.