Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ China and Russia references for government and public-sector closure patterns,
and South American public-servant references with common administrative bridge
or year-end closure days.

The calendars include recent statutory changes such as Ireland's Saint Brigid's
Day, Poland's Christmas Eve holiday, Italy's Saint Francis of Assisi Day from
2026, and Ukraine's revised holiday dates. Published New Zealand Matariki dates
are covered through 2052, while the explicit East Asian lunar reference tables
currently cover Chinese New Year and Chuseok through 2035. Islamic dates are
calendar estimates and can differ by a day from an official local moon-sighting
announcement; regional and one-off government closure announcements should
therefore be treated as reference signals rather than payroll calendars.

The generic activity analysis compares weekly business-hours against
weekend/off-hours activity, or yearly weekday activity against weekend activity.
It is intended as a broad activity-label heuristic rather than a declaration of
Expand Down
69 changes: 61 additions & 8 deletions tempolocus/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1196,7 +1196,7 @@ def _uk_holidays(year: int, easter: date) -> list[Holiday]:


def _ireland_holidays(year: int, easter: date) -> list[Holiday]:
return [
holidays = [
_fixed(year, 1, 1, "New Year's Day"),
_fixed(year, 3, 17, "Saint Patrick's Day"),
_relative(easter, 1, "Easter Monday"),
Expand All @@ -1207,6 +1207,18 @@ def _ireland_holidays(year: int, easter: date) -> list[Holiday]:
_fixed(year, 12, 25, "Christmas Day"),
_fixed(year, 12, 26, "Saint Stephen's Day"),
]
# Ireland introduced the Saint Brigid's Day public holiday in 2023. It is
# observed on 1 February when that is a Friday, otherwise on the first
# Monday in February.
if year >= 2023:
february_first = date(year, 2, 1)
brigid_day = (
february_first
if february_first.weekday() == 4
else _nth_weekday(year, 2, 0, 1, "Saint Brigid's Day").day
)
holidays.append(Holiday(brigid_day, "Saint Brigid's Day"))
return sorted(holidays, key=lambda holiday: (holiday.day, holiday.name))


def _france_holidays(year: int, easter: date) -> list[Holiday]:
Expand Down Expand Up @@ -1240,7 +1252,7 @@ def _germany_holidays(year: int, easter: date) -> list[Holiday]:


def _italy_holidays(year: int, easter: date) -> list[Holiday]:
return [
holidays = [
_fixed(year, 1, 1, "New Year's Day"),
_fixed(year, 1, 6, "Epiphany"),
_relative(easter, 1, "Easter Monday"),
Expand All @@ -1253,6 +1265,10 @@ def _italy_holidays(year: int, easter: date) -> list[Holiday]:
_fixed(year, 12, 25, "Christmas Day"),
_fixed(year, 12, 26, "Saint Stephen's Day"),
]
# The restored national feast of Saint Francis takes effect in 2026.
if year >= 2026:
holidays.append(_fixed(year, 10, 4, "Saint Francis of Assisi Day"))
return sorted(holidays, key=lambda holiday: (holiday.day, holiday.name))


def _spain_holidays(year: int, easter: date) -> list[Holiday]:
Expand Down Expand Up @@ -1347,7 +1363,7 @@ def _austria_holidays(year: int, easter: date) -> list[Holiday]:


def _poland_holidays(year: int, easter: date) -> list[Holiday]:
return [
holidays = [
_fixed(year, 1, 1, "New Year's Day"),
_fixed(year, 1, 6, "Epiphany"),
_relative(easter, 1, "Easter Monday"),
Expand All @@ -1360,6 +1376,9 @@ def _poland_holidays(year: int, easter: date) -> list[Holiday]:
_fixed(year, 12, 25, "Christmas Day"),
_fixed(year, 12, 26, "Second Day of Christmas"),
]
if year >= 2025:
holidays.append(_fixed(year, 12, 24, "Christmas Eve"))
return sorted(holidays, key=lambda holiday: (holiday.day, holiday.name))


def _czechia_holidays(year: int, easter: date) -> list[Holiday]:
Expand Down Expand Up @@ -1484,19 +1503,28 @@ def _serbia_holidays(year: int, orthodox_easter: date) -> list[Holiday]:


def _ukraine_holidays(year: int, orthodox_easter: date) -> list[Holiday]:
return [
holidays = [
_fixed(year, 1, 1, "New Year's Day"),
_fixed(year, 1, 7, "Orthodox Christmas Day"),
_fixed(year, 3, 8, "International Women's Day"),
Holiday(orthodox_easter, "Orthodox Easter Sunday"),
_relative(orthodox_easter, 49, "Orthodox Pentecost"),
_fixed(year, 5, 1, "Labour Day"),
_fixed(year, 5, 8, "Day of Remembrance and Victory"),
_fixed(year, 6, 28, "Constitution Day"),
_fixed(year, 8, 24, "Independence Day"),
_fixed(year, 10, 14, "Defenders Day"),
_fixed(year, 12, 25, "Christmas Day"),
]
if year < 2023:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Ukraine's pre-repeal 2023 holidays

For Ukrainian yearly input from 2023, this year-only cutoff rewrites holidays that had already occurred before the relevant laws took effect: January 7 and May 9 were still statutory holidays on those dates, while the replacement May 8 holiday was enacted only afterward. As a result, _candidate_holidays(2023) now omits two actual 2023 reference signals and adds one retroactively, which can distort Ukraine's score; 2023 needs a boundary-specific calendar rather than being handled entirely by the post-change branch.

Useful? React with 👍 / 👎.

holidays.append(_fixed(year, 1, 7, "Orthodox Christmas Day"))
holidays.append(_fixed(year, 10, 14, "Defenders Day"))
holidays.append(_fixed(year, 5, 9, "Victory Day"))
else:
holidays.append(_fixed(year, 5, 8, "Day of Remembrance and Victory"))
holidays.append(_fixed(year, 10, 1, "Defenders Day"))
if year in (2022, 2023):
holidays.append(_fixed(year, 7, 28, "Ukrainian Statehood Day"))
elif year >= 2024:
holidays.append(_fixed(year, 7, 15, "Ukrainian Statehood Day"))
return sorted(holidays, key=lambda holiday: (holiday.day, holiday.name))


def _russia_holidays(year: int, orthodox_easter: date) -> list[Holiday]:
Expand Down Expand Up @@ -1919,6 +1947,11 @@ def _chinese_new_year(year: int) -> date | None:
2028: (1, 26),
2029: (2, 13),
2030: (2, 3),
2031: (1, 23),
2032: (2, 11),
2033: (1, 31),
2034: (2, 19),
2035: (2, 8),
}
if year not in dates:
return None
Expand All @@ -1941,6 +1974,11 @@ def _korean_chuseok(year: int) -> date | None:
2028: (10, 3),
2029: (9, 22),
2030: (9, 12),
2031: (10, 1),
2032: (9, 19),
2033: (9, 8),
2034: (9, 27),
2035: (9, 16),
}
if year not in dates:
return None
Expand Down Expand Up @@ -2178,7 +2216,7 @@ def _australia_holidays(year: int, easter: date) -> list[Holiday]:


def _new_zealand_holidays(year: int, easter: date) -> list[Holiday]:
return [
holidays = [
_fixed(year, 1, 1, "New Year's Day"),
_fixed(year, 1, 2, "Day after New Year's Day"),
_fixed(year, 2, 6, "Waitangi Day"),
Expand All @@ -2190,6 +2228,21 @@ def _new_zealand_holidays(year: int, easter: date) -> list[Holiday]:
_fixed(year, 12, 25, "Christmas Day"),
_fixed(year, 12, 26, "Boxing Day"),
]
# Legislated Matariki dates are deliberately explicit: the public holiday
# follows the maramataka rather than a Gregorian weekday formula.
matariki_dates = {
2022: (6, 24), 2023: (7, 14), 2024: (6, 28), 2025: (6, 20),
2026: (7, 10), 2027: (6, 25), 2028: (7, 14), 2029: (7, 6),
2030: (6, 21), 2031: (7, 11), 2032: (7, 2), 2033: (6, 24),
2034: (7, 7), 2035: (6, 29), 2036: (7, 18), 2037: (7, 10),
2038: (6, 25), 2039: (7, 15), 2040: (7, 6), 2041: (7, 19),
2042: (7, 11), 2043: (7, 3), 2044: (6, 24), 2045: (7, 7),
2046: (6, 29), 2047: (7, 19), 2048: (7, 3), 2049: (6, 25),
2050: (7, 15), 2051: (6, 30), 2052: (6, 21),
}
if matariki := matariki_dates.get(year):
holidays.append(_fixed(year, *matariki, "Matariki"))
return sorted(holidays, key=lambda holiday: (holiday.day, holiday.name))


def _gregorian_from_jdn(jdn: int) -> date:
Expand Down
41 changes: 41 additions & 0 deletions tests/test_tempolocus.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,47 @@ def test_standard_holiday_profile_includes_expanded_europe_regions():
)


def test_recent_european_public_holiday_changes_are_date_aware():
candidates = _candidate_holidays(2026)

assert ("2026-02-02", "Saint Brigid's Day") in {
(holiday.day.isoformat(), holiday.name) for holiday in candidates["IE"][2]
}
assert ("2026-10-04", "Saint Francis of Assisi Day") in {
(holiday.day.isoformat(), holiday.name) for holiday in candidates["IT"][2]
}
assert ("2026-12-24", "Christmas Eve") in {
(holiday.day.isoformat(), holiday.name) for holiday in candidates["PL"][2]
}
ukraine = {
(holiday.day.isoformat(), holiday.name) for holiday in candidates["UA"][2]
}
assert ("2026-10-01", "Defenders Day") in ukraine
assert not any(day == "2026-01-07" for day, _name in ukraine)


def test_new_zealand_matariki_period_uses_published_dates():
assert ("2026-07-10", "Matariki") in {
(holiday.day.isoformat(), holiday.name)
for holiday in _candidate_holidays(2026)["NZ"][2]
}
assert ("2052-06-21", "Matariki") in {
(holiday.day.isoformat(), holiday.name)
for holiday in _candidate_holidays(2052)["NZ"][2]
}


def test_lunar_holiday_reference_periods_extend_through_2035():
candidates = _candidate_holidays(2035)

assert ("2035-02-08", "Chinese New Year") in {
(holiday.day.isoformat(), holiday.name) for holiday in candidates["SG"][2]
}
assert ("2035-09-16", "Chuseok") in {
(holiday.day.isoformat(), holiday.name) for holiday in candidates["KR"][2]
}


def test_json_timestamp_list_is_detected_and_aggregated_as_weekly_activity():
data = [
"2026-01-05T09:15:00Z",
Expand Down
Loading