Capstone Toolkit
Date and Time in Practice
Chapter 10 ยท Capstone Toolkit
This final chapter puts the course together. You will build a small scheduling toolkit with four parts: one function that turns "local date, wall time and zone" into a correct moment, a generator for weekly and monthly recurring events, a search for the next occurrence after a given moment, and a finder for working hours that several zones share. Then you will test it against the traps from earlier chapters. Every snippet was run with Python 3.14; the comments show what was printed.
The Design in Four Rules
- A recurring event is a rule, not a list of instants. "Mondays at 09:00 in London" is stored as weekdays, a wall time and a zone name (Chapter 9, future local appointments).
- Generate each occurrence from the local date. Step through calendar dates in the rule's zone, attach the wall time, then convert. Never add
timedelta(days=7)to one UTC instant (Chapter 5). - Return aware values only, and reject naive input at the boundary (Chapter 3).
- Decide every edge case once, in one place: a wall time that does not exist, a wall time that happens twice, and a day number that a month does not have.
Step 1: Resolving a Local Time
The core of the toolkit is a function that turns a date, a wall time and a zone name into one definite moment. Its policy for the two awkward cases is deliberately simple: a time that falls in a spring gap moves forward by the length of the gap, and a time that happens twice takes its first occurrence (fold=0).
The round trip through UTC is what repairs the gap: zoneinfo gives the non-existent 01:30 the offset from before the change, so converting to UTC and back lands on 02:30 BST. For an ambiguous time the same call returns the first of the two, offset +01:00. If your product needs a different policy (skip the occurrence, or use the second one), this is the one function to change.
Step 2: Recurrence
A weekly rule walks forward one calendar day at a time and keeps the days whose weekday is in the set. A monthly rule fixes a day number and clamps it to the last day of shorter months (Chapter 6), so "the 31st" becomes the 28th in February instead of failing.
The weekly output is the whole lesson of the course in four lines: the wall time stays at 09:00 while the UTC time moves from 09:00 to 08:00 when British Summer Time begins on 28 March. A calendar app that stored the first occurrence as a UTC instant and repeated it every 7 days would show 10:00 after the change.
Step 3: The Next Occurrence
Most real code does not need the whole series, only "when is the next one after now?". Start from today's date in the rule's zone, which may differ from the UTC date, and take the first candidate that is strictly later than now. Looking ahead 8 days is enough to see every weekday at least once.
Step 4: Finding a Shared Window
To schedule one meeting for people in several zones, check each UTC hour of the day and keep the hours that fall inside everyone's working hours. Because it works from the actual date, daylight saving is accounted for without any special code:
The same two cities have a three-hour shared window on 2 November and a four-hour window on 16 March, because on 16 March New York is already on daylight time and London is not, so the gap between them is four hours instead of five. Add Tokyo and there is no shared hour at all. An empty result is a legitimate answer, so the caller must handle it (for example by widening the hours or rotating the unfriendly time between the zones).
Testing the Toolkit
Date code is easy to get almost right, so the tests are not an extra: they are how you know it works. Choose cases at the boundaries, state the expected value by hand, and use fixed dates rather than the current time so the tests give the same result every day:
Writing the last test taught a useful lesson: the author's first guess for the expected value was three hours, and the test failed with four. The test was right and the guess was wrong, because the gap between the cities that week was four hours, not five. That is exactly why you work out the expected value from first principles and then let the code argue with you.
Course Checklist
- Types and text (1-2): use ISO 8601 for text; never rely on locale-dependent or day/month-ambiguous formats.
- Naive vs aware (3): a naive value is not a moment; attach a zone or reject it.
- Zones (4): IANA names, UTC for storage and comparison, local time for display.
- Daylight saving (5): a day is not always 24 hours; handle the gap and the repeated hour on purpose.
- Calendar (6-7): clamp month ends, mind leap years, use half-open intervals.
- Timestamps (8): know the unit, and the limits of the type that holds them.
- Storage (9): instants in UTC, calendar dates as dates, future local events as wall time plus zone name.
- Always: test the boundaries, with fixed dates, and work out the expected answer by hand first.
Challenges
A team stand-up happens every Tuesday and Thursday at 10:00 New York time, starting on 2026-10-27. Using the toolkit's resolve and weekly, list the first six occurrences, and for each print the New York time with its zone abbreviation, the UTC time, and the London time with its abbreviation. Predict, before running, which rows change their UTC hour and why London's time is the same as UTC for every row.
Use resolve to schedule a daily 01:30 London job for 27 to 30 March 2027, printing the local time with its abbreviation and the UTC time for each day. Then build 01:30 on 25 October 2026 in London with fold=0 and fold=1 and print both in local and UTC time. Predict which day's 01:30 is moved and to what, and how far apart the two folds are. Finish by comparing the two fold values with == and explain the result.
Write next_after(now, t, zone, weekdays), which returns the first occurrence strictly after an aware now as a UTC datetime and rejects a naive now. Then write a unittest suite with five tests: a later time on the same day, a now that is exactly on the occurrence, a search that crosses a clock change, a zone whose date is ahead of the UTC date, and a naive input. Work out all expected values by hand before running the suite.
Quick Reference
- Recurring event: store weekdays or day number + wall time + IANA zone name, never a list of UTC instants
- Resolve:
datetime.combine(d, t, tzinfo=z).astimezone(UTC).astimezone(z)repairs a gap and picks the first of a repeated hour - Generate: step through local calendar dates, attach the wall time, then convert
- Month ends:
min(day, calendar.monthrange(y, m)[1]) - Next occurrence: start from today's date in the rule's zone and require the result to be strictly after
now - Shared window: test each UTC hour against every zone's local hours; an empty list is a valid result
- Compare: convert to UTC first; same-zone aware values compare by wall clock and ignore
fold - Test: fixed dates, boundary cases, expected values worked out by hand