diff --git a/README.md b/README.md index 545006a..4970015 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. + diff --git a/tempolocus/core.py b/tempolocus/core.py index d2b8a1c..bca7994 100644 --- a/tempolocus/core.py +++ b/tempolocus/core.py @@ -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.""" @@ -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" @@ -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] = ( @@ -2971,3 +2980,4 @@ def _algeria_holidays(year: int) -> list[Holiday]: ], _arab_islamic_holiday_set(year), ) + diff --git a/tempolocus/country_references.py b/tempolocus/country_references.py new file mode 100644 index 0000000..2753586 --- /dev/null +++ b/tempolocus/country_references.py @@ -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 diff --git a/tests/test_country_coverage.py b/tests/test_country_coverage.py new file mode 100644 index 0000000..b2f9557 --- /dev/null +++ b/tests/test_country_coverage.py @@ -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() diff --git a/tests/test_tempolocus.py b/tests/test_tempolocus.py index 114d4e8..ba8e46e 100644 --- a/tests/test_tempolocus.py +++ b/tests/test_tempolocus.py @@ -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"]} @@ -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 @@ -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 { @@ -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 +