The project
You have spent this module on habits: practise by doing, read errors, ask for help well and keep a steady routine. Now you will build the tool that checks the routine part. It is a study tracker: a program that reads your learning log and prints a report. It tells you how much you studied, how long your longest run of days was, where you broke the never-miss-twice rule, how your weeks add up and which topics are due for review today.
The assignment uses everything in the module in a concrete way.
- The learning log (Lesson 5) becomes the input of the program.
- Spaced review (Lessons 1 and 5) becomes the "due today" list.
- Never miss twice (Lesson 5) becomes a check on the dates.
- The weekly rhythm (Lesson 5) becomes a weekly summary.
- Reading errors and debugging (Lesson 2) are how you will fix your own mistakes along the way.
- Using the tutor well (Lessons 3 and 4) is how you should get unstuck. Try alone first, ask for a hint and verify with the examples below.
You may use any of the assistants and the tutor box as you work, under the rules of this module: try first, ask with five parts, never paste what you do not understand, and test the result. The point is that you can explain every line of your final program.
The language is Python, because the runnable examples in this module use it. Everything you need appears in the programs of the earlier lessons: reading input, loops, dictionaries, dates, f-strings and functions. If a feature is new to you, a short search or a question to the tutor will explain it, and the hints below mention which ones to look at.
The specification
Input
The program reads from standard input.
- Line 1: today's date in the form
YYYY-MM-DD. - Every following line: one study session in the form
date minutes topic, where the date isYYYY-MM-DD, the minutes is a whole number and the topic is the rest of the line (it may contain spaces). - Blank lines are ignored.
- The sessions may be in any order. More than one session may share a date.
- There may be no sessions at all.
Here is a sample input. Each line after the first is one session.
2026-11-12
2026-11-02 60 relative paths
2026-11-03 45 git init
2026-11-05 90 git commit
2026-11-06 30 branches
2026-11-09 120 pull requests
2026-11-11 45 python inputOutput
The program prints a report in exactly this order and format. Lines that depend on data are described below the sample.
Sessions: 6
Total time: 6h 30m
Study days: 6
Average per study day: 65 min
Longest streak: 2 days
Missed twice in a row:
2026-11-06 to 2026-11-09: 2 days missed
Weeks:
week of 2026-11-02: 225 min on 4 days
week of 2026-11-09: 165 min on 2 days
Reviews due on 2026-11-12:
python input (studied 1 day ago)
pull requests (studied 3 days ago)
git commit (studied 7 days ago)The rules for each part:
- Sessions is the number of session lines.
- Total time is all minutes added up, written as hours and minutes with two digits for the minutes:
6h 30m,0h 05m,1h 45m. - Study days is the number of different dates that have at least one session.
- Average per study day is the total minutes divided by the study days, rounded down to a whole number. Print this line only when there is at least one study day.
- Longest streak is the longest run of consecutive calendar days with study, counting a day once even if it has several sessions. Write
1 dayfor one andN daysfor others. With no sessions the streak is0 days. - Missed twice in a row lists every gap between two study days in which two or more whole days were skipped. For each, print the two study dates and the number of days skipped. A gap of one skipped day is allowed by the rule and is not listed. If there are none, print
Missed twice in a row: noneon a single line. Otherwise print the heading line and then one indented line per gap, in date order. - Weeks groups the study days by calendar week, where a week starts on Monday. For each week with study, print the Monday date, the total minutes and the number of study days, in date order. If there are no sessions, print
Weeks: none. - Reviews due lists topics whose review falls on the date in line 1. A session studied on day D is due for review at D plus 1, D plus 3 and D plus 7 days. A line shows the topic and how many days ago it was studied (
1 day ago,3 days ago,7 days ago). Order them by days ago, smallest first, and alphabetically by topic when the days ago are equal. If nothing is due, printReviews due on DATE: none.
Use the singular day when the number is exactly 1 and days otherwise, everywhere a count of days appears.
A second example
This input has sessions out of order, two sessions on the same day and a Sunday as the first date. Check each rule against it.
2026-11-04
2026-11-03 20 loops
2026-11-03 25 lists
2026-11-01 60 terminalSessions: 3
Total time: 1h 45m
Study days: 2
Average per study day: 52 min
Longest streak: 1 day
Missed twice in a row: none
Weeks:
week of 2026-10-26: 60 min on 1 day
week of 2026-11-02: 45 min on 1 day
Reviews due on 2026-11-04:
lists (studied 1 day ago)
loops (studied 1 day ago)
terminal (studied 3 days ago)Why does it print this? The three sessions are on two dates, the 1st and the 3rd. The total is 105 minutes, which is 1h 45m. The average over two study days is 52.5, rounded down to 52. The 1st and the 3rd have one skipped day between them (the 2nd), which the rule allows, so nothing is listed. The 1st is a Sunday, so it belongs to the week that started on Monday 26 October. The 3rd is in the week of Monday 2 November. For the reviews due on the 4th, the two sessions on the 3rd are one day old, and the one on the 1st is three days old.
The empty example
With no sessions, only the date:
2026-11-12Sessions: 0
Total time: 0h 00m
Study days: 0
Longest streak: 0 days
Missed twice in a row: none
Weeks: none
Reviews due on 2026-11-12: noneNote that the average line is missing because there are no study days. Dividing by zero would crash the program, so the rule says to skip the line, which is an example of the edge-case thinking from Lesson 3.
Requirements checklist
Copy this list into your notes and tick items as you finish them.
- The program reads all of standard input and takes today's date from line 1.
- It handles topics that contain spaces.
- It ignores blank lines.
- It prints the session count and the total time as
Xh YYm. - It counts study days as distinct dates, so two sessions on one date count once.
- It prints the average per study day, rounded down, and omits the line when there are no study days.
- It finds the longest streak of consecutive days and uses
dayordayscorrectly. - It finds every gap of two or more skipped days and prints them in date order, or prints
none. - It groups study days by week with Monday as the first day, and prints minutes and days for each week.
- It prints the reviews due on today's date at 1, 3 and 7 days, in the required order.
- It gives the exact output of the three examples above.
- It works with sessions in any order.
- The code is split into functions, with names that say what each one does.
- You can explain every line to someone else.
- You wrote three log entries about building it, in the format from Lesson 5.
How it will be graded
The rubric describes three levels. Aim for the top one, and check yourself honestly.
| Score | What the solution looks like |
|---|---|
| 60% | Runs without crashing on the first example and prints the right session count, total time, study days and average. The streak and the weekly totals may be missing or wrong. The code is one long block, but it works for the simple case. |
| 80% | Everything in the first example is exactly right, including streaks, the missed-twice report, weekly totals and the reviews due. It also gives correct output for the second example (unsorted input, two sessions on a day). The empty input may crash or print an extra line. The code uses a few functions. |
| 100% | Exact output for all three examples and for edge cases you invent: one session, sessions only on a Sunday, a gap of exactly two days, a gap of one day, the same topic on several days. Singular and plural are right. The code is in clear functions, has no repeated logic, and you can explain each part. You tested your own edge cases and recorded them. You also wrote the log entries. |
Two further ideas help you check your own work. First, always read the output character by character against the examples, since the exact format matters in this assignment. Second, a program that gives the right output for your test data may still be wrong for another input, so make three inputs of your own and work out the answers by hand before you run the program.
A starter to run
This runnable program reads the input and prints the first line of the report. Run it with the first example and then extend it, one rule at a time.
Type the first example into the Input box, and it prints Sessions: 6. Notice that it reads every line at once with sys.stdin.read().splitlines(). The first line is the date, and the remaining lines are sessions, with blank lines removed.
Work in small steps, as in Lesson 1: add one output line, run, compare, and then add the next. After each step, ask what you expect before you run it.
Hints
Try for at least 15 minutes before opening any hint. Open them one at a time, starting from the first, and stop as soon as you can continue.
Hint 1: reading the input
The date on line 1 can be turned into a date object with date.fromisoformat(text) from the datetime module. For each session line, line.split(maxsplit=2) splits at the first two spaces only, so the topic keeps its own spaces. The three pieces are the date text, the minutes text and the topic. Convert the first with date.fromisoformat and the second with int.
Hint 2: totals and study days
Keep a dictionary that maps each date to the minutes studied on it. For every session, add its minutes to totals.get(day, 0). The number of study days is the length of the dictionary, and the total time is the sum of its values. The total in hours and minutes comes from integer division by 60 and the remainder, as in the session planner of Lesson 5. For two-digit minutes use {minutes % 60:02d}.
Hint 3: streaks and gaps
Sort the dates. Walk through them in pairs, using zip(days, days[1:]). For each pair, (after - before).days is the difference in days. A difference of 1 means the next calendar day, so a run continues. Any other difference starts a new run of length 1. Keep the best run length so far. For the missed-twice report, a difference of d means d - 1 skipped days, and you report the pair when d - 1 is at least 2. The never-miss-twice checker in Lesson 5 does almost the same thing.
Hint 4: weeks
A date has a method weekday() that returns 0 for Monday and 6 for Sunday. Subtracting timedelta(days=day.weekday()) from the date gives the Monday of its week. Use that Monday as the key of a dictionary. For each key, keep two numbers: the total minutes and the count of study days. Print the keys in sorted order.
Hint 5: reviews due
For each session and each gap in (1, 3, 7), check whether the session date plus timedelta(days=gap) equals today's date. If it does, remember the pair (gap, topic). Then sorted puts them in order by gap, and then by topic, because Python compares tuples one item at a time. Use the same plural helper for 1 day ago.
Hint 6: singular and plural
Write one small function, plural(n, word), that returns the number, a space and the word, with an s added unless the number is 1. Use it everywhere a count of days appears, so the rule lives in one place.
Hint 7: if the output differs by one line
Compare your output with the expected one using a text comparison, not your eyes. Save both in files and compare them with the diff command in the terminal. A difference in a single space or in day against days is the usual cause. Check the lines that print none: they are a different format (a single line) from the lines that print a list (a heading and indented lines).
Reference solution
Do not open this until you have a working program of your own, or until you have spent a real effort and want to see how another solution is organised. If you open it early, close it and rewrite from memory afterwards, as Lesson 1 advises.
Reference solution
import sys
from datetime import date, timedelta
GAPS = (1, 3, 7)
def parse(lines):
today = date.fromisoformat(lines[0].strip())
sessions = []
for line in lines[1:]:
line = line.strip()
if not line:
continue
day, minutes, topic = line.split(maxsplit=2)
sessions.append((date.fromisoformat(day), int(minutes), topic))
return today, sessions
def minutes_by_day(sessions):
totals = {}
for day, minutes, _ in sessions:
totals[day] = totals.get(day, 0) + minutes
return totals
def longest_streak(days):
best = current = 0
previous = None
for day in days:
current = current + 1 if previous and (day - previous).days == 1 else 1
best = max(best, current)
previous = day
return best
def missed_twice(days):
found = []
for before, after in zip(days, days[1:]):
missed = (after - before).days - 1
if missed >= 2:
found.append((before, after, missed))
return found
def week_start(day):
return day - timedelta(days=day.weekday())
def plural(n, word):
return f"{n} {word}" + ("" if n == 1 else "s")
def hours_text(minutes):
return f"{minutes // 60}h {minutes % 60:02d}m"
def main():
today, sessions = parse(sys.stdin.read().splitlines())
totals = minutes_by_day(sessions)
days = sorted(totals)
print(f"Sessions: {len(sessions)}")
print(f"Total time: {hours_text(sum(totals.values()))}")
print(f"Study days: {len(days)}")
if days:
print(f"Average per study day: {sum(totals.values()) // len(days)} min")
print(f"Longest streak: {plural(longest_streak(days), 'day')}")
gaps = missed_twice(days)
print("Missed twice in a row:" + ("" if gaps else " none"))
for before, after, missed in gaps:
print(f" {before} to {after}: {plural(missed, 'day')} missed")
weeks = {}
for day in days:
entry = weeks.setdefault(week_start(day), [0, 0])
entry[0] += totals[day]
entry[1] += 1
print("Weeks:" + ("" if weeks else " none"))
for start in sorted(weeks):
minutes, count = weeks[start]
print(f" week of {start}: {minutes} min on {plural(count, 'day')}")
due = []
for day, _, topic in sessions:
for gap in GAPS:
if day + timedelta(days=gap) == today:
due.append((gap, topic))
print(f"Reviews due on {today}:" + ("" if due else " none"))
for gap, topic in sorted(due):
print(f" {topic} (studied {plural(gap, 'day')} ago)")
main()How the solution is organised, so you can compare it with yours.
parseturns the raw lines into a date and a list of tuples. Parsing lives in one place, so the rest of the program works with dates and numbers, not text.minutes_by_daybuilds the dictionary of minutes per date. Everything about study days comes from it, which is why two sessions on one date count as one study day.longest_streakandmissed_twiceboth work on the sorted list of dates and look at neighbouring pairs. The streak asks whether the difference is exactly one. The gap check asks whether the number of skipped days is at least two.week_startfinds the Monday of a week, and the weekly totals use it as a dictionary key.pluralandhours_textare tiny helpers that remove repeated formatting.maincalls the helpers in the order of the report. The conditional expressions such as("" if gaps else " none")choose between the one-linenoneform and the heading form.
The review check loops over every session and every gap and compares dates. For a log of a few hundred sessions that is instant. Sorting the tuples (gap, topic) gives the order the specification asks for without extra code.
Test your own edge cases
A program that passes the examples can still fail on inputs you did not think of. Write at least four inputs of your own and work out the expected output by hand before you run the program. Some ideas:
- One session only, on the same day as today.
- Two sessions with exactly two days between them (one skipped day) and then another with three days between (two skipped days). Which is reported?
- A gap that crosses a month end, such as 2026-10-30 and 2026-11-02.
- All sessions on Sundays, to check the week grouping.
- The same topic studied on days 1, 3 and 7 before today, so three lines of the same topic appear in the review list.
If the program disagrees with your hand answer, first check your hand answer, and then the program. Printing the intermediate values (the sorted dates, the dictionary of weeks) is the first tool, exactly as in the debugging walkthrough of Lesson 2.
Stretch goals
Only start these when the main program passes every example.
- Add a final line
Best week: week of DATE with N minthat names the week with the most minutes. - Read a target of minutes per week from an optional second input and flag any week below it with the word
below. - Print a bar of
#marks for each study day, one mark per 15 minutes, using the weekly-hours program of Lesson 5 as a model. - Handle bad input kindly: if a line cannot be read, print
skipped line N: <the line>to the output and continue instead of crashing. Usetryandexceptas in the error zoo of Lesson 2.
Write it up
Finish with the habit that this module has taught. Write three entries in your learning log while you build: what you learned at the end of a session, what you were stuck on, and what you will do next. At the end, write a short note, four or five sentences, on which help level you used and where you were stuck the longest. If you used an assistant or the tutor, say what you asked and how you checked the answer. A record like that is the kind of story an interviewer likes to hear, and it is the log you will use to review the module in a week.