Summaries by Day
Learning Objectives
- You can compute per-day totals using the same two-pass pattern as per-subject totals, with the date as the category.
- You can return the daily totals in chronological order using ISO date strings.
- You can select the heaviest day with a “best so far” loop, honouring an explicit tie policy.
- You can compose date-window selection with daily totals to produce “the last seven days.”
The per-subject pattern from the Summaries by Category chapter was: find the distinct subjects, then loop again to summarise each one. The same shape works when the category is the date instead of the subject. How many minutes did I study each day? is the same question as how many minutes per subject?, just with a different field.
This chapter applies the pattern, picks out the heaviest day with the “best so far” loop from the previous chapter, and composes the result with date windows.
Per-Day Totals
The function follows the Summaries by Category shape exactly, with day_string where subject used to be:
def totals_by_day(sessions): """Return [date_string, total_minutes] rows, sorted by date ascending.
Returns [] for empty input. """ days_seen = [] for subject, minutes, day in sessions: if day not in days_seen: days_seen.append(day) days_seen.sort()
result = [] for day in days_seen: total = 0 for subject, minutes, row_day in sessions: if row_day == day: total += minutes result.append([day, total]) return resultThe same two-pass approach, the same outer-loop-over-categories, the same inner-loop-over-sessions.
The extra bit is the days_seen.sort() step. Now, the function returns its rows in date order, which is what every caller wants. The sort is a small extra cost on top of the two-pass pattern, but it gives you the right order for free — you do not have to write a more complicated loop to insert rows in the right place.
A test using the same fixture as before:
sessions = [ ["math", 45, "2026-05-10"], ["physics", 90, "2026-05-10"], ["history", 30, "2026-05-11"], ["math", 60, "2026-05-11"], ["math", 25, "2026-05-12"],]
assert totals_by_day(sessions) == [ ["2026-05-10", 135], ["2026-05-11", 90], ["2026-05-12", 25],]assert totals_by_day([]) == []print("totals_by_day tests passed.")The arithmetic: May 10 has 45 + 90 = 135 minutes. May 11 has 30 + 60 = 90. May 12 has just 25. Output is in date order because of the sort.
The Heaviest Day
Once totals_by_day exists, “which day had the most minutes?” is the “best so far” loop from the previous chapter, applied to the daily totals:
def heaviest_day(sessions): """Return the [date_string, total_minutes] row for the heaviest day.
Ties are broken by the earliest tied day. Returns None for empty input. """ totals = totals_by_day(sessions) if len(totals) == 0: return None
best = totals[0] for row in totals[1:]: if row[1] > best[1]: best = row return bestThe function calls totals_by_day (which already returns rows sorted by date). If the result is empty, the function returns None. Otherwise the “best so far” loop walks the rows and keeps the largest one.
A test:
assert heaviest_day([]) is Noneassert heaviest_day(sessions) == ["2026-05-10", 135]And a test for the tie behaviour:
tied = [ ["math", 60, "2026-05-10"], ["math", 60, "2026-05-11"],]assert heaviest_day(tied) == ["2026-05-10", 60]Recent Daily Totals: Composing Windows and Days
To find recent daily totals, you can compose the sessions_between function from Working With Dates and Date Windows with totals_by_day:
from datetime import date, timedelta
def recent_daily_totals(sessions, today=None): """Return totals_by_day for the last seven days ending at today (inclusive).
Returns [] for empty input or no sessions in the window. """ if today is None: today = date.today() start = today - timedelta(days=6) recent = sessions_between(sessions, start, today) return totals_by_day(recent)The function is a few lines of work, all of which are already-tested pieces. sessions_between filters the sessions to the date window. totals_by_day summarises them.
A test:
sessions = [ ["math", 45, "2026-05-10"], ["physics", 90, "2026-05-10"], ["history", 30, "2026-05-11"], ["math", 60, "2026-05-11"], ["math", 25, "2026-05-12"], ["math", 20, "2026-02-15"], # well outside the window]today = date.fromisoformat("2026-05-12")
assert recent_daily_totals(sessions, today) == [ ["2026-05-10", 135], ["2026-05-11", 90], ["2026-05-12", 25],]The row from the past is correctly excluded — it falls outside the seven-day window. The remaining rows are grouped by day and totalled.
The next exercise transfers recent-window grouping to route logs, where the category is not the date but the filter still depends on dates.
Weekly minutes by route
0 / 15 points
Write a function named weekly_minutes_by_route(logs, today=None).
Each row in logs has this shape:
[route, minutes, day]The function must return route totals for today and the previous six days:
[route, total_minutes]Requirements:
- use the provided
logs_between(logs, start, end)helper to filter by date window, - use
date.today()whentodayisNone, - include both today and the date six days before today,
- skip rows outside the seven-day window,
- group totals by route using lists and loops only,
- do not use dictionaries,
- sort result rows alphabetically by route,
- return
[]when no rows are inside the window, - do not print anything,
- and do not use
input(...).
A Note on the Repeated Pattern
This is the third time you have written code with this shape: the per-subject totals, the per-subject summaries, and now the per-day totals. Each one finds the unique values of a category, then walks the data once per category to summarise.
Take a moment and look at what those three functions have in common. They all need a place to map each category to a running summary. Lists do not give them one, so the code creates it — with the unique-values list as the keys and the inner loop as the lookup. Every variant of this pattern pays the same cost: an inner loop that almost always runs and almost always does the same thing.
Putting It Together
The functions in this chapter feed directly into the project’s dashboard:
totals_by_day(sessions) # for any per-day analysisheaviest_day(sessions) # for "Heaviest day:" linerecent_daily_totals(sessions, today) # for "Recent days:" blockEach one is small. Each one is built from the date and sorting tools you already have, applied with patterns you have seen before. None of them needs anything beyond lists, ordinary loops, and the standard built-ins.
Check Your Understanding
- Why does
totals_by_daysortdays_seenbefore the second pass? - The tie policy for
heaviest_daysays “earliest tied day wins.” Where in the code is this policy actually enforced? Two things work together — what are they? - What does
recent_daily_totalsdo when no sessions fall inside the seven-day window? How does this follow from the functions it composes? - Why do
recent_daily_totalsandtotal_in_last_seven_daysboth taketoday=None? What does this give you that hard-codingdate.today()does not?
AI Study Aid
Create a chapter diagram
We're looking into to what extent AI-generated diagrams could help with studying.
Use this study aid to generate an AI-generated visual summary of the material you just studied.
Each diagram style emphasizes the content in a different way, so you can choose the focus
that feels most useful.
Using the diagram generator is voluntary and does not affect your course progress. We encourage
you to try it out and see if it helps, but it's totally up to you! Your answers help us understand
how to make better study aids in the future.