skip to content

Which calendar boundaries — leap day, a daylight-saving change, a year end — deserve deliberate cases?

level: seniorimportance: nice to knowfreq 31%

answer

  1. Today is the least interesting date
  2. Some times never happen, some happen twice
  3. Not every day has 24 hours
  4. Divisible by 100 but not 400
  5. Week numbering disagrees at year end

basics

~20 s

Calendar arithmetic breaks where ordinary assumptions fail: 29 February, the daylight-saving days that are 23 or 25 hours long, and a year end where week numbering diverges from the calendar year. Choose those dates deliberately instead of testing on today.

solid answer

~50 s

Pick the boundaries from the arithmetic the code actually performs. Three families cover most of it. **Existence**: 29 February has no counterpart in most years, and on a spring-forward day some local wall-clock times never occur. **Length**: a local day can be 23 or 25 hours, a year is not always 365 days, and month lengths differ, so adding a fixed number of hours or days is not adding a day or a year. **Numbering**: at a year end, week numbering and fiscal periods stop agreeing with the calendar year, and a date in late December can belong to the first week of the next year. Add the quieter ones: some zones are offset by 30 or 45 minutes, and on a fall-back day a local timestamp is ambiguous because it names two instants. Pin each case to a chosen instant and a named zone, and prefer past dates, whose rules are settled.

code

pseudocode · 8 lines
pseudocode
case "fall-back day has 25 local hours":
    zone  = namedZone("a zone with a daylight-saving change")
    start = localMidnight("2021-11-07", zone)
    end   = localMidnight("2021-11-08", zone)
    expect hoursBetween(start, end) == 25

case "anniversary of a leap day in a common year":
    expect addYears(date("2020-02-29"), 1) == date("2021-02-28")

go deeper

for a junior

Be ready to name a few boundary dates and say what is odd about each: 29 February, the two daylight-saving days, and the last days of a year. Knowing they deserve their own cases is enough here.

for a middle

Explain the mechanics behind each boundary: the leap-year rule including the century exception, why a local day can be 23 or 25 hours, and why adding fixed hours is not adding a day.

for a senior

Show how you pick boundaries from the code's actual arithmetic rather than a checklist, pin the instant and the zone in the case, and assert the semantic outcome instead of a formatted string.

for a principal

Own the policy question: how much date risk the product carries, whether the team should stop truncating by local day at all, and where a single time-handling convention should be imposed across services.

## Why "today" is a bad test case Date logic written against today's date passes on almost every day of the year. The defects live on a handful of specific days, and they reach production because those days arrive on their own schedule — usually a Sunday morning in spring, or the twenty-ninth of February four years after the code was written. The useful discipline is to choose boundary dates from the arithmetic the code actually performs, not from a memorised list. Ask what the code does with time. Does it only stamp and compare instants? Then boundary dates buy very little. Does it add months or years, truncate to a local day, bucket into hours, or number weeks? Then each of those operations has a boundary where its everyday assumption stops holding. ## Family one: existence Some dates and times simply do not exist. **29 February** exists only in leap years, so adding one year to it has no obvious answer — implementations usually clamp to the 28th, and code that expects an exact anniversary quietly disagrees. The Gregorian rule is worth having at hand: a year divisible by 4 is a leap year, except one divisible by 100, unless it is also divisible by 400. So 2000 was a leap year, 2100 will not be, and any code testing only "divisible by four" is wrong in a way that has a known arrival date. **Spring forward.** When clocks jump forward, a band of local wall-clock times never occurs on that date. Constructing one either raises an error or silently shifts to a neighbouring instant, and both behaviours have burned schedulers that store a local time of day. ## Family two: length **A local day is not always 24 hours.** On a spring-forward day it is 23; on a fall-back day it is 25. Code that computes "the next day" by adding 24 hours to a local midnight lands an hour early or an hour late, and code that expects a day's hourly buckets to number 24 either overflows or leaves a gap. **A year is not 365 days and a month is not 30.** Adding 365 days to mean "a year later" drifts across a leap year, and adding 30 days to mean "a month later" drifts every month. Month-end arithmetic has its own boundary: the last day of January plus one month is a date that does not exist. **Offsets are not always whole hours.** Several zones sit at 30 or 45 minutes past an hour, which breaks code that assumes an offset can be represented in whole hours or that a local day boundary aligns with an hour boundary in a zero-offset zone. ## Family three: numbering and ambiguity **Year end.** Week-numbering schemes assign a week to the year containing most of it, so a date in late December can fall in week 1 of the following year and a date in early January can fall in the last week of the previous one. Fiscal years, retention windows and expiry periods add their own boundaries that rarely coincide with 1 January. **Fall back.** After clocks go back, a local wall-clock time in the repeated hour names two distinct instants. This is the quietest of all the boundary defects, because constructing such a time usually succeeds — one of the two offsets is chosen for you — so nothing raises and the result is simply wrong half the time. ## A worked example A fleet telematics ingest computes a daily distance rollup per vehicle, bucketed by local day, and an hourly breakdown keyed on the local hour label. Two boundary cases were added deliberately. On the fall-back day the local day is 25 hours long. Two different hours both carry the label 01, so the hourly map collides and the second hour's readings overwrite the first for each of the 1,243 vehicles — the daily total stays right while the breakdown silently loses an hour. On the spring-forward day the opposite: the local day is 23 hours, the code's "midnight plus 24 hours" boundary lands at 01:00 the following day, and the first hour of the next day is counted twice. Neither case is reachable from today's date, and neither would have been found by generating more random days: they are two specific days a year, in one zone. ## Practical rules Pin each boundary case to an explicit instant and an explicitly named zone, so the case means the same thing on every machine. Prefer past dates: a region can change its daylight-saving rules, and a case pinned to a future transition can start failing when the platform's timezone rule data is updated — a genuine failure of the test, not of the product. Assert the semantic outcome (which bucket, how many hours, which anniversary date) rather than a formatted string, so the case survives a formatting change. And name the case after the boundary it guards, because in three years the number 29 in a date literal will not explain itself.

  • Why prefer a past daylight-saving date to a future one in a pinned case?
    Because a region can change its rules. The platform's timezone rule data is updated as governments legislate, so a case pinned to a transition that has not happened yet can start failing when the rule data is refreshed, even though the product is unchanged. A past transition is settled history and the case stays meaningful.
  • Which daylight-saving direction produces the quieter defect, and why?
    Falling back. A local time in the repeated hour names two instants, and constructing it normally succeeds by picking one offset, so nothing raises and the answer is simply wrong some of the time. Springing forward gives a local time that does not exist, which more often raises an error or shifts visibly, so it tends to be found sooner.
  • How do you decide whether a piece of code deserves boundary-date cases at all?
    By looking at the arithmetic. Code that only stamps and compares instants gains little. Code that adds months or years, truncates or groups by local day, counts hourly buckets, or numbers weeks has an everyday assumption at each of those operations, and the boundary case is simply the day that assumption stops holding.

A calendar is not a ruler. Days are not all the same length, years are not all the same number of days, and some marks on the scale appear twice or not at all.

saying these in an interview costs you the question

  • Assumes every local day is exactly 24 hours
  • Thinks every year divisible by four is a leap year
  • Adds 365 days to mean one year later
  • Assumes every zone offset is a whole hour
  • Tests date logic on today and calls it covered
  • Believes a local timestamp always names one instant

context