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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,30 @@ because that input shape contains weekdays and hours but no calendar dates.
| 🕌 Middle East | 🇧🇭 Bahrain, 🇮🇷 Iran, 🇮🇱 Israel, 🇯🇴 Jordan, 🇰🇼 Kuwait, 🇱🇧 Lebanon, 🇴🇲 Oman, 🇶🇦 Qatar, 🇸🇦 Saudi Arabia, 🇦🇪 United Arab Emirates |
| 🌎 South America | 🇦🇷 Argentina, 🇧🇷 Brazil, 🇨🇱 Chile, 🇨🇴 Colombia, 🇵🇪 Peru, 🇺🇾 Uruguay |

### Additional country references (partial calendars)

The detailed calendars above cover 80 countries. An additional 117 countries
have **fixed-date reference signals**, bringing yearly country coverage to
197: the 193 UN members, the Holy See and Palestine, plus Kosovo (`XK`)
and Taiwan (`TW`). This is a coverage convention, not a statement on sovereignty.

These additional candidates are explicitly labeled `fixed-date references
(partial)` in JSON and text results. They contain selected national and civic
anniversaries only. They omit movable religious holidays, observed-day shifts,
regional holidays and one-off closures, and do not reconstruct historical
holiday laws. A reference date does not imply every worker has the day off.
The same partial references are available in both holiday profiles; the
public-worker profile does not add government closure rules for them.

Afghanistan (`AF`), Albania (`AL`), Andorra (`AD`), Angola (`AO`), Antigua and Barbuda (`AG`), Armenia (`AM`), Azerbaijan (`AZ`), Bahamas (`BS`), Bangladesh (`BD`), Barbados (`BB`), Belarus (`BY`), Belize (`BZ`), Benin (`BJ`), Bhutan (`BT`), Bolivia (`BO`), Botswana (`BW`), Brunei (`BN`), Burkina Faso (`BF`), Burundi (`BI`), Cabo Verde (`CV`), Cambodia (`KH`), Cameroon (`CM`), Central African Republic (`CF`), Chad (`TD`), Comoros (`KM`), Republic of the Congo (`CG`), Democratic Republic of the Congo (`CD`), Cyprus (`CY`), Djibouti (`DJ`), Dominica (`DM`), Ecuador (`EC`), El Salvador (`SV`), Equatorial Guinea (`GQ`), Eritrea (`ER`), Eswatini (`SZ`), Fiji (`FJ`), Gabon (`GA`), Gambia (`GM`), Georgia (`GE`), Grenada (`GD`), Guinea (`GN`), Guinea-Bissau (`GW`), Guyana (`GY`), Haiti (`HT`), Honduras (`HN`), Indonesia (`ID`), Iraq (`IQ`), Jamaica (`JM`), Kazakhstan (`KZ`), Kiribati (`KI`), Kosovo (`XK`), Kyrgyzstan (`KG`), Laos (`LA`), Lesotho (`LS`), Liberia (`LR`), Libya (`LY`), Liechtenstein (`LI`), Madagascar (`MG`), Malawi (`MW`), Maldives (`MV`), Mali (`ML`), Malta (`MT`), Marshall Islands (`MH`), Mauritania (`MR`), Mauritius (`MU`), Micronesia (`FM`), Moldova (`MD`), Monaco (`MC`), Mongolia (`MN`), Montenegro (`ME`), Mozambique (`MZ`), Myanmar (`MM`), Namibia (`NA`), Nauru (`NR`), Nepal (`NP`), Nicaragua (`NI`), Niger (`NE`), North Macedonia (`MK`), Pakistan (`PK`), Palau (`PW`), Palestine (`PS`), Papua New Guinea (`PG`), Paraguay (`PY`), Rwanda (`RW`), Saint Kitts and Nevis (`KN`), Saint Lucia (`LC`), Saint Vincent and the Grenadines (`VC`), Samoa (`WS`), San Marino (`SM`), Sao Tome and Principe (`ST`), Seychelles (`SC`), Sierra Leone (`SL`), Solomon Islands (`SB`), Somalia (`SO`), South Sudan (`SS`), Sri Lanka (`LK`), Sudan (`SD`), Suriname (`SR`), Syria (`SY`), Taiwan (`TW`), Tajikistan (`TJ`), Tanzania (`TZ`), Timor-Leste (`TL`), Togo (`TG`), Tonga (`TO`), Trinidad and Tobago (`TT`), Türkiye (`TR`), Turkmenistan (`TM`), Tuvalu (`TV`), Uganda (`UG`), Uzbekistan (`UZ`), Vanuatu (`VU`), Vatican City (`VA`), Venezuela (`VE`), Yemen (`YE`), Zambia (`ZM`), Zimbabwe (`ZW`).

Reference dates are maintained in `tempolocus/country_references.py`;
[national days](https://en.wikipedia.org/wiki/National_day) provide a starting
point for checking national anniversaries. Broader country coverage does not
make these sparse references equivalent to the detailed calendars above.
Weekly country inference remains based on matching multiple timezone offsets;
these additions apply to yearly holiday inference.

Yearly analysis treats a lack of activity on holidays as the default signal;
pass `--activity-signal peak` when unusually high activity is the indicator you
want to match instead. Yearly analysis defaults to public holidays and separately
Expand Down Expand Up @@ -317,3 +341,4 @@ results:

This project is licensed under the GNU Affero General Public License v3.0 or
later. See [LICENSE](LICENSE) for details.

10 changes: 10 additions & 0 deletions tempolocus/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from statistics import mean, median
from typing import Any, Iterable

from .country_references import fixed_date_references


class DetectionError(ValueError):
"""Raised when an input file cannot be interpreted."""
Expand Down Expand Up @@ -505,6 +507,7 @@ def infer_yearly(
"analysis": activity_analysis,
"assumptions": [
"Daily buckets are compared with representative public-holiday and religious-observance calendars.",
"Candidates labeled fixed-date references (partial) contain selected civic anniversaries only; movable holidays and observed-day substitutions are omitted.",
(
"The public-worker holiday profile adds public-sector closure days where they are distinct from general public holidays."
if holiday_profile == "public-worker"
Expand Down Expand Up @@ -1040,6 +1043,12 @@ def _candidate_holidays(
"ET": ("ET", "Ethiopia", _ethiopia_holidays(year)),
"ZA": ("ZA", "South Africa", _south_africa_holidays(year, easter)),
}
for code, label, references in fixed_date_references(year):
candidates.setdefault(code, (
code,
f"{label} / fixed-date references (partial)",
[Holiday(day, name) for day, name in references],
))
for variant in ("Bastani", "Fasli", "Qadimi", "Shahanshahi"):
region_id = f"IR-ZOROASTRIAN-{variant.upper()}"
candidates[region_id] = (
Expand Down Expand Up @@ -2971,3 +2980,4 @@ def _algeria_holidays(year: int) -> list[Holiday]:
],
_arab_islamic_holiday_set(year),
)

144 changes: 144 additions & 0 deletions tempolocus/country_references.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Fixed-date reference signals for countries without a detailed calendar.

These are deliberately partial modern calendars, not statutory holiday lists.
National-day dates can be checked against https://en.wikipedia.org/wiki/National_day
and each country's public-holiday calendar. Movable holidays, observed-day
substitutions, historical regimes and one-off closures are not reconstructed.
Keep these separate from the detailed calendars so callers can report coverage.
"""

# ISO 3166-1 alpha-2 codes, plus XK for Kosovo. Each row is
# code | display name | MM-DD holiday name; ...
FIXED_DATE_REFERENCES = """
AF|Afghanistan|08-19 Independence Day
AL|Albania|03-14 Summer Day;11-28 Independence Day;11-29 Liberation Day
AD|Andorra|03-14 Constitution Day;09-08 Our Lady of Meritxell
AO|Angola|02-04 Liberation Day;09-17 National Heroes Day;11-11 Independence Day
AG|Antigua and Barbuda|11-01 Independence Day;12-09 National Heroes Day
AM|Armenia|01-28 Army Day;05-28 Republic Day;09-21 Independence Day
AZ|Azerbaijan|05-28 Independence Day;06-15 National Salvation Day;11-08 Victory Day
BS|Bahamas|07-10 Independence Day;12-26 Boxing Day
BD|Bangladesh|02-21 Language Martyrs Day;03-26 Independence Day;12-16 Victory Day
BB|Barbados|04-28 National Heroes Day;08-01 Emancipation Day;11-30 Independence Day
BY|Belarus|05-09 Victory Day;07-03 Independence Day;11-07 October Revolution Day
BZ|Belize|09-10 Saint George's Caye Day;09-21 Independence Day
BJ|Benin|01-10 Traditional Religions Day;08-01 Independence Day
BT|Bhutan|02-21 King's Birthday;12-17 National Day
BO|Bolivia|01-22 Plurinational State Day;08-06 Independence Day
BW|Botswana|07-01 Sir Seretse Khama Day;09-30 Botswana Day
BN|Brunei|02-23 National Day;07-15 Sultan's Birthday
BF|Burkina Faso|08-05 Independence Day;12-11 Republic Day
BI|Burundi|02-05 Unity Day;07-01 Independence Day;10-21 President Ndadaye Day
CV|Cabo Verde|01-20 National Heroes Day;07-05 Independence Day
KH|Cambodia|01-07 Victory Day;09-24 Constitution Day;11-09 Independence Day
CM|Cameroon|02-11 Youth Day;05-20 National Day
CF|Central African Republic|03-29 Boganda Day;08-13 Independence Day;12-01 Republic Day
TD|Chad|08-11 Independence Day;12-01 Freedom and Democracy Day
KM|Comoros|07-06 Independence Day;11-12 Maore Day
CG|Republic of the Congo|08-15 Independence Day;11-28 Republic Day
CD|Democratic Republic of the Congo|01-17 Lumumba Day;06-30 Independence Day
CY|Cyprus|04-01 National Day;10-01 Independence Day
DJ|Djibouti|06-27 Independence Day
DM|Dominica|11-03 Independence Day;11-04 Community Service Day
EC|Ecuador|05-24 Battle of Pichincha;08-10 First Cry of Independence;10-09 Guayaquil Independence Day
SV|El Salvador|08-06 Feast of the Divine Saviour;09-15 Independence Day
GQ|Equatorial Guinea|08-03 Freedom Day;10-12 Independence Day
ER|Eritrea|05-24 Independence Day;06-20 Martyrs Day;09-01 Revolution Day
SZ|Eswatini|04-19 King's Birthday;09-06 Independence Day
FJ|Fiji|09-07 Constitution Day;10-10 Fiji Day
GA|Gabon|08-17 Independence Day
GM|Gambia|02-18 Independence Day
GE|Georgia|04-09 National Unity Day;05-26 Independence Day;11-23 Saint George's Day
GD|Grenada|02-07 Independence Day;10-25 Thanksgiving Day
GN|Guinea|10-02 Independence Day
GW|Guinea-Bissau|01-20 National Heroes Day;09-24 Independence Day
GY|Guyana|02-23 Republic Day;05-26 Independence Day;08-01 Emancipation Day
HT|Haiti|01-01 Independence Day;05-18 Flag Day;11-18 Battle of Vertieres
HN|Honduras|09-15 Independence Day
ID|Indonesia|06-01 Pancasila Day;08-17 Independence Day
IQ|Iraq|01-06 Army Day;10-03 National Day;12-10 Victory Day
JM|Jamaica|08-01 Emancipation Day;08-06 Independence Day
KZ|Kazakhstan|05-07 Defender of the Fatherland Day;08-30 Constitution Day;10-25 Republic Day;12-16 Independence Day
KI|Kiribati|07-12 Independence Day
XK|Kosovo|02-17 Independence Day;04-09 Constitution Day
KG|Kyrgyzstan|05-05 Constitution Day;08-31 Independence Day
LA|Laos|03-08 International Women's Day;12-02 National Day
LS|Lesotho|03-11 Moshoeshoe Day;07-17 King's Birthday;10-04 Independence Day
LR|Liberia|03-15 Joseph Jenkins Roberts Birthday;07-26 Independence Day;08-24 Flag Day
LY|Libya|02-17 Revolution Day;09-16 Martyrs Day;12-24 Independence Day
LI|Liechtenstein|08-15 National Day;12-08 Immaculate Conception
MG|Madagascar|03-29 Martyrs Day;06-26 Independence Day
MW|Malawi|03-03 Martyrs Day;07-06 Independence Day
MV|Maldives|07-26 Independence Day;11-11 Republic Day
ML|Mali|01-20 Army Day;09-22 Independence Day
MT|Malta|03-31 Freedom Day;06-07 Sette Giugno;09-08 Victory Day;09-21 Independence Day;12-13 Republic Day
MH|Marshall Islands|05-01 Constitution Day;11-17 President's Day
MR|Mauritania|11-28 Independence Day
MU|Mauritius|02-01 Abolition of Slavery;03-12 Independence and Republic Day;11-02 Arrival of Indentured Labourers
FM|Micronesia|05-10 Constitution Day;11-03 Independence Day
MD|Moldova|08-27 Independence Day;08-31 National Language Day
MC|Monaco|01-27 Saint Devota's Day;11-19 National Day
MN|Mongolia|07-11 Naadam;07-12 Naadam;07-13 Naadam;11-26 Republic Day
ME|Montenegro|05-21 Independence Day;05-22 Independence Day;07-13 Statehood Day;07-14 Statehood Day
MZ|Mozambique|02-03 Heroes Day;06-25 Independence Day;09-25 Armed Forces Day;10-04 Peace and Reconciliation Day
MM|Myanmar|01-04 Independence Day;02-12 Union Day;03-27 Armed Forces Day
NA|Namibia|03-21 Independence Day;05-04 Cassinga Day;08-26 Heroes Day
NR|Nauru|01-31 Independence Day;10-26 Angam Day
NP|Nepal|09-20 Constitution Day
NI|Nicaragua|09-14 Battle of San Jacinto;09-15 Independence Day
NE|Niger|04-24 Concord Day;08-03 Independence Day;12-18 Republic Day
MK|North Macedonia|08-02 Republic Day;09-08 Independence Day;10-11 Uprising Day
PK|Pakistan|03-23 Pakistan Day;08-14 Independence Day;12-25 Quaid-e-Azam Day
PW|Palau|07-09 Constitution Day;10-01 Independence Day
PS|Palestine|11-15 Independence Day
PG|Papua New Guinea|07-23 National Remembrance Day;08-26 National Repentance Day;09-16 Independence Day
PY|Paraguay|05-14 Independence Day;05-15 Independence Day;06-12 Chaco Armistice Day
RW|Rwanda|02-01 Heroes Day;07-01 Independence Day;07-04 Liberation Day
KN|Saint Kitts and Nevis|09-16 National Heroes Day;09-19 Independence Day
LC|Saint Lucia|02-22 Independence Day;12-13 National Day
VC|Saint Vincent and the Grenadines|03-14 National Heroes Day;10-27 Independence Day
WS|Samoa|06-01 Independence Day
SM|San Marino|02-05 Liberation Day;09-03 Foundation Day;10-01 Investiture of Captains Regent
ST|Sao Tome and Principe|02-03 Martyrs Day;07-12 Independence Day;09-06 Armed Forces Day
SC|Seychelles|06-18 Constitution Day;06-29 Independence Day
SL|Sierra Leone|04-27 Independence Day
SB|Solomon Islands|07-07 Independence Day
SO|Somalia|06-26 Independence Day;07-01 Republic Day
SS|South Sudan|05-16 SPLA Day;07-09 Independence Day;07-30 Martyrs Day
LK|Sri Lanka|02-04 Independence Day
SD|Sudan|01-01 Independence Day
SR|Suriname|07-01 Emancipation Day;11-25 Independence Day
SY|Syria|04-17 Evacuation Day;05-06 Martyrs Day
TW|Taiwan|02-28 Peace Memorial Day;10-10 National Day
TJ|Tajikistan|06-27 National Unity Day;09-09 Independence Day;11-06 Constitution Day
TZ|Tanzania|04-26 Union Day;10-14 Nyerere Day;12-09 Independence Day
TL|Timor-Leste|05-20 Restoration of Independence;11-28 Proclamation of Independence
TG|Togo|04-27 Independence Day
TO|Tonga|06-04 Emancipation Day;07-04 King's Birthday;11-04 Constitution Day
TT|Trinidad and Tobago|08-01 Emancipation Day;08-31 Independence Day;09-24 Republic Day
TR|Türkiye|04-23 National Sovereignty and Children's Day;05-19 Youth and Sports Day;08-30 Victory Day;10-29 Republic Day
TM|Turkmenistan|05-18 Constitution Day;09-27 Independence Day;12-12 Neutrality Day
TV|Tuvalu|10-01 Independence Day;10-02 Independence Day
UG|Uganda|01-26 Liberation Day;06-09 Heroes Day;10-09 Independence Day
UZ|Uzbekistan|09-01 Independence Day;10-01 Teachers Day;12-08 Constitution Day
VU|Vanuatu|07-30 Independence Day;10-05 Constitution Day
VA|Vatican City|02-11 Lateran Treaty Day;06-29 Saints Peter and Paul
VE|Venezuela|04-19 Declaration of Independence;07-05 Independence Day;07-24 Simon Bolivar Birthday
YE|Yemen|05-22 Unity Day;09-26 Revolution Day;10-14 Liberation Day;11-30 Independence Day
ZM|Zambia|03-12 Youth Day;05-25 Africa Freedom Day;10-24 Independence Day
ZW|Zimbabwe|04-18 Independence Day;05-25 Africa Day;12-22 Unity Day
""".strip()


def fixed_date_references(year: int):
"""Yield (code, country name, [(date, holiday name), ...])."""
from datetime import date

for row in FIXED_DATE_REFERENCES.splitlines():
code, label, entries = row.split("|")
holidays = []
for entry in entries.split(";"):
day, name = entry.split(" ", 1)
month, day = map(int, day.split("-"))
holidays.append((date(year, month, day), name))
yield code, label, holidays
69 changes: 69 additions & 0 deletions tests/test_country_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Country coverage and reference-calendar regression checks."""

import unittest
from datetime import date

from tempolocus import detect
from tempolocus.core import _candidate_holidays
from tempolocus.country_references import fixed_date_references


# 193 UN members, the two UN observer states, Kosovo and Taiwan.
EXPECTED_COUNTRIES = set("""
AF AL DZ AD AO AG AR AM AU AT AZ BS BH BD BB BY BE BZ BJ BT BO BA BW BR BN BG
BF BI CV KH CM CA CF TD CL CN CO KM CG CD CR CI HR CU CY CZ DK DJ DM DO EC EG
SV GQ ER EE SZ ET FJ FI FR GA GM GE DE GH GR GD GT GN GW GY HT VA HN HU IS IN
ID IR IQ IE IL IT JM JP JO KZ KE KI KP KR KW KG LA LV LB LS LR LY LI LT LU MG
MW MY MV ML MT MH MR MU MX FM MD MC MN ME MA MZ MM NA NR NP NL NZ NI NE NG MK
NO OM PK PW PS PA PG PY PE PH PL PT QA RO RU RW KN LC VC WS SM ST SA SN RS SC
SL SG SK SI SB SO ZA SS ES LK SD SR SE CH SY TW TJ TZ TH TL TG TO TT TN TR TM
TV UG UA AE GB US UY UZ VU VE VN YE ZM ZW XK
""".split())


class CountryCoverageTests(unittest.TestCase):
def test_all_countries_in_both_profiles(self):
for public_worker in (False, True):
candidates = _candidate_holidays(2026, public_worker)
self.assertEqual(EXPECTED_COUNTRIES, {k for k in candidates if len(k) == 2})

def test_partial_calendars_are_explicit_and_dates_stay_in_year(self):
for year in (2024, 2026, 2035):
candidates = _candidate_holidays(year)
rows = list(fixed_date_references(year))
self.assertEqual(len(rows), len({row[0] for row in rows}))
for code, _label, references in rows:
self.assertIn('(partial)', candidates[code][1])
self.assertTrue(references)
self.assertTrue(all(day.year == year for day, _ in references))

def test_country_specific_dates(self):
candidates = _candidate_holidays(2026)
for code, month, day, name in (
('ID', 8, 17, 'Independence Day'),
('PK', 3, 23, 'Pakistan Day'),
('EC', 5, 24, 'Battle of Pichincha'),
('TZ', 4, 26, 'Union Day'),
('MT', 9, 21, 'Independence Day'),
('VU', 7, 30, 'Independence Day'),
):
self.assertIn((date(2026, month, day), name),
{(h.day, h.name) for h in candidates[code][2]})

def test_existing_detailed_calendars_remain_detailed(self):
candidates = _candidate_holidays(2026, True)
self.assertEqual('France', candidates['FR'][1])
self.assertIn('IR-SHIA', candidates)
self.assertIn('CN-PUBLIC-WORKER', candidates)
self.assertTrue(any(h.name == 'Bastille Day' for h in candidates['FR'][2]))

def test_new_country_is_available_in_yearly_inference(self):
result = detect({'year': 2026, 'max': 10,
'nb': [['2026-01-01', 10], ['2026-12-31', 10]]}, top=1000)
indexed = {item['id']: item for item in result['results']}
self.assertIn('ID', indexed)
self.assertIn('(partial)', indexed['ID']['label'])


if __name__ == '__main__':
unittest.main()
7 changes: 4 additions & 3 deletions tests/test_tempolocus.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_kind_can_be_forced():


def test_public_worker_holiday_profile_adds_sector_references():
result = detect(load_sample("year.json"), top=120, holiday_profile="public-worker")
result = detect(load_sample("year.json"), top=1000, holiday_profile="public-worker")

assert result["signals"]["holiday_profile"] == "public-worker"
ids = {item["id"] for item in result["results"]}
Expand Down Expand Up @@ -86,7 +86,7 @@ def test_standard_holiday_profile_includes_orthodox_regions():


def test_public_worker_holiday_profile_adds_china_and_russia_references():
result = detect(load_sample("year.json"), top=120, holiday_profile="public-worker")
result = detect(load_sample("year.json"), top=1000, holiday_profile="public-worker")

ids = {item["id"] for item in result["results"]}
assert "CN-PUBLIC-WORKER" in ids
Expand Down Expand Up @@ -221,7 +221,7 @@ def test_chile_indigenous_peoples_day_uses_winter_solstice_date():


def test_public_worker_holiday_profile_adds_south_american_references():
result = detect(load_sample("year.json"), top=120, holiday_profile="public-worker")
result = detect(load_sample("year.json"), top=1000, holiday_profile="public-worker")

ids = {item["id"] for item in result["results"]}
assert {
Expand Down Expand Up @@ -572,3 +572,4 @@ def test_cli_accepts_period_filter(tmp_path, capsys):
assert exit_code == 0
output = json.loads(capsys.readouterr().out)
assert output["signals"]["timestamps_seen"] == 1

Loading