Purpose
A holiday flag answers "is this local calendar date a national holiday in this country" — used to explain otherwise-anomalous demand, traffic, or audience patterns without a manual lookup table. This KB computes holidays from rules (fixed dates, the nth or last weekday of a month, and the Gregorian Easter computus) rather than from a scraped or licensed calendar feed. That makes the result small, auditable, and reproducible for any year, at the cost of completeness: v0 covers US federal, UK bank, and Canada national holidays only, and does not attempt regional, lunar, or locally-observed holidays. Critically, a holiday is not a slot — it is a 24-local-hour band, i.e. a slot-set once mapped to UTC, exactly like a broadcast day.
Source and destination
Source: a local_date (YYYY-MM-DD) and a country code. Destination: a
holiday_flag (matched holiday name, if any) and, when mapped to UTC for
measurement, a slot_set covering that local calendar date's 24 hours.
Exactness: approximate
approximate. The rule set is exact for what
it models (a fixed date always falls on that date; an nth-weekday rule always
resolves the same way; the Easter computus is a well-defined deterministic
algorithm), but the coverage is approximate relative to "all holidays
that matter" in a given country: it omits state/provincial holidays,
substitute (in-lieu) days by default, and any lunar or movable holiday not
already enumerated. Treat a false result as "not flagged by this rule set,"
not as an authoritative "not a holiday anywhere in this jurisdiction."
Algorithm
import { holidaysFor, isHoliday, easterSunday } from "@/lib/time/holidays";
holidaysFor("US", 2026).find((h) => h.name === "Thanksgiving");
// -> { date: "2026-11-26", name: "Thanksgiving", country: "US" }
// (4th Thursday of November — nthWeekday(year, month=11, isoWeekday=Thursday, n=4))
easterSunday(2026);
// -> { month: 4, day: 5 } (2026-04-05, via the Anonymous/Meeus computus)
isHoliday("2026-04-05", "UK");
// -> null — Easter SUNDAY itself is not a UK bank holiday; Good Friday
// (2026-04-03) and Easter Monday (2026-04-06) are, and are computed as
// offsets from easterSunday() rather than looked up separately.
isHoliday("2026-07-04", "US");
// -> { date: "2026-07-04", name: "Independence Day", country: "US" }
Each country's holiday list is a small, explicit array built from three
primitives: a literal fixed date (iso(y, 12, 25) for Christmas), an
nth-weekday rule (nthWeekday(y, 1, 1, 3) for the third Monday in January —
Martin Luther King Jr. Day), and a last-weekday rule (lastWeekday(y, 5, 1)
for the last Monday in May — Memorial Day). Easter-derived UK holidays
compute an offset in days from easterSunday(y) rather than encoding their
own date rule, so they stay correct in every year without a separate lookup
table.
Parameters
- country
- One of US | UK | CA in v0. Each has its own rule builder.
- year
- Calendar year the rules are evaluated for; all rules are computable for any year, past or future.
- dateISO
- For isHoliday: the local calendar date to test, YYYY-MM-DD.
Outputs
holidaysFor returns every Holiday ({date, name, country}) the rule set
produces for that country and year. isHoliday returns the matching
Holiday or null. easterSunday returns {month, day} for the Gregorian
Easter Sunday of a given year — the anchor several UK holidays are computed
from.
Units and convention
Holiday dates are local calendar dates (YYYY-MM-DD) in the country's own civil calendar, not UTC instants. Mapping a holiday to the KB's canonical UTC slots requires an explicit zone and produces a 24-hour slot-set (which, like a broadcast day, may be 23 or 25 UTC hours across a DST transition in that country) rather than a single slot.
DST and disambiguation behavior
Holiday date computation itself has no DST dependency — it is pure calendar arithmetic. DST only enters once a holiday's local date is converted to a UTC slot-set for measurement, at which point the same gap/fold handling as DST Handling applies to the conversion, not to the holiday rule.
Quality and provenance
This is intentionally a small, auditable rule set, not a substitute for an authoritative feed anywhere legal observance matters (payroll, banking closures, contractual SLAs). State the tzdb/rule-set version alongside any holiday-flag output, and do not silently extend v0's three-country coverage by inference — an unmodeled country should report "unknown," not "not a holiday."
Edge cases
Substitute (in-lieu) holiday days: when a fixed-date holiday lands on a Saturday or Sunday, many countries (UK, much of APAC) observe a substitute weekday instead — this rule set does not apply in-lieu substitution, so a fixed-date holiday falling on a weekend is flagged on its nominal date only, which will disagree with the country's actual observed closure date. Movable and regional holidays: lunar-calendar holidays (Eid, Diwali, Lunar New Year) shift against the Gregorian calendar year to year and are not covered by this rule set at all; regional holidays (US state, Canadian province) are omitted from the national-only lists above.
Python parity
def easter_sunday(year: int) -> tuple[int, int]:
a = year % 19
b, c = divmod(year, 100)
d, e = divmod(b, 4)
f = (b + 8) // 25
g = (b - f + 1) // 3
h = (19 * a + b - d - g + 15) % 30
i, k = divmod(c, 4)
l = (32 + 2 * e + 2 * i - h - k) % 7
m = (a + 11 * h + 22 * l) // 451
month = (h + l - 7 * m + 114) // 31
day = (h + l - 7 * m + 114) % 31 + 1
return month, day # e.g. easter_sunday(2026) == (4, 5)
The tested reference implementation is the TypeScript in lib/time/holidays.ts;
this Python is the identical Anonymous/Meeus computus (integer division in
place of Math.floor), producing the same (month, day) for every year. The
fixed-date and nth/last-weekday rules translate directly using
calendar.monthrange or manual weekday arithmetic and are omitted here for
brevity — the algorithm is the same as nthWeekday/lastWeekday above.
