From 651c91890d665eea465a81be856c173abf6b4eab Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 13:13:02 +0530 Subject: [PATCH 01/33] Update EID_AL_FITR confirmed year to 2026 Indonesia: add 2026 Eid al-Fitr confirmed date (MAR 21) Signed-off-by: akshiDhi --- holidays/countries/indonesia.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holidays/countries/indonesia.py b/holidays/countries/indonesia.py index 31fbf88b2c..c834a7d2dc 100644 --- a/holidays/countries/indonesia.py +++ b/holidays/countries/indonesia.py @@ -294,7 +294,7 @@ class IndonesiaIslamicHolidays(_CustomIslamicHolidays): 2024: (JUN, 17), } - EID_AL_FITR_DATES_CONFIRMED_YEARS = (1963, 2025) + EID_AL_FITR_DATES_CONFIRMED_YEARS = (1963, 2026) EID_AL_FITR_DATES = { 1963: (FEB, 25), 1964: (FEB, 15), @@ -328,6 +328,7 @@ class IndonesiaIslamicHolidays(_CustomIslamicHolidays): 2019: (JUN, 5), 2023: (APR, 22), 2025: (MAR, 31), + 2026: (MAR, 21), } HIJRI_NEW_YEAR_DATES_CONFIRMED_YEARS = (1968, 2025) From 8550906113d1f679cbd43abbb8b2930fe196d498 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 13:15:09 +0530 Subject: [PATCH 02/33] Update EID_AL_FITR confirmed year to 2026 Philippines: add 2026 Eid al-Fitr confirmed date (MAR 20) Signed-off-by: akshiDhi --- holidays/countries/philippines.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holidays/countries/philippines.py b/holidays/countries/philippines.py index afbdbc50ea..5ae07703f7 100644 --- a/holidays/countries/philippines.py +++ b/holidays/countries/philippines.py @@ -218,7 +218,7 @@ class PhilippinesIslamicHolidays(_CustomIslamicHolidays): 2024: (JUN, 17), } - EID_AL_FITR_DATES_CONFIRMED_YEARS = (2002, 2025) + EID_AL_FITR_DATES_CONFIRMED_YEARS = (2002, 2026) EID_AL_FITR_DATES = { 2002: (DEC, 6), 2003: (NOV, 26), @@ -235,6 +235,7 @@ class PhilippinesIslamicHolidays(_CustomIslamicHolidays): 2020: (MAY, 25), 2022: (MAY, 3), 2025: (APR, 1), + 2026: (MAR, 20), } From dbe49a3a4580563fa41cd8d5719a82a6933a5076 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 13:19:30 +0530 Subject: [PATCH 03/33] Update EID_AL_FITR confirmed year to 2026 Indonesia: add 2025-2026 Eid al-Adha confirmed dates Signed-off-by: akshiDhi --- holidays/countries/indonesia.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holidays/countries/indonesia.py b/holidays/countries/indonesia.py index c834a7d2dc..50fb66bfb6 100644 --- a/holidays/countries/indonesia.py +++ b/holidays/countries/indonesia.py @@ -255,7 +255,7 @@ class IndonesiaChineseHolidays(_CustomChineseHolidays): class IndonesiaIslamicHolidays(_CustomIslamicHolidays): - EID_AL_ADHA_DATES_CONFIRMED_YEARS = (1963, 2025) + EID_AL_ADHA_DATES_CONFIRMED_YEARS = (1963, 2026) EID_AL_ADHA_DATES = { 1963: (MAY, 4), 1964: (APR, 23), @@ -292,6 +292,8 @@ class IndonesiaIslamicHolidays(_CustomIslamicHolidays): 2022: (JUL, 10), 2023: (JUN, 29), 2024: (JUN, 17), + 2025: (JUN, 6), + 2026: (MAY, 27), } EID_AL_FITR_DATES_CONFIRMED_YEARS = (1963, 2026) From cacf5a095830ca92a5524dccbee9edb87fa589b9 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 13:22:23 +0530 Subject: [PATCH 04/33] Update EID_AL_Adha confirmed year to 2026 Philippines: add 2025-2026 Eid al-Adha confirmed dates Signed-off-by: akshiDhi --- holidays/countries/philippines.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holidays/countries/philippines.py b/holidays/countries/philippines.py index 5ae07703f7..b0da5643e3 100644 --- a/holidays/countries/philippines.py +++ b/holidays/countries/philippines.py @@ -206,7 +206,7 @@ class PhilippinesChineseHolidays(_CustomChineseHolidays): class PhilippinesIslamicHolidays(_CustomIslamicHolidays): - EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2010, 2025) + EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2010, 2026) EID_AL_ADHA_DATES = { 2010: (NOV, 17), 2011: (NOV, 7), @@ -216,6 +216,8 @@ class PhilippinesIslamicHolidays(_CustomIslamicHolidays): 2017: (SEP, 2), 2019: (AUG, 12), 2024: (JUN, 17), + 2025: (JUN, 6), + 2026: (MAY, 27), } EID_AL_FITR_DATES_CONFIRMED_YEARS = (2002, 2026) From 8d64af176fd69092207c046ed09d09ce35663d5a Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 15:52:16 +0530 Subject: [PATCH 05/33] Philippines: add missing MAR import Signed-off-by: akshiDhi --- holidays/countries/philippines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holidays/countries/philippines.py b/holidays/countries/philippines.py index b0da5643e3..3bee33fe0c 100644 --- a/holidays/countries/philippines.py +++ b/holidays/countries/philippines.py @@ -13,7 +13,7 @@ from gettext import gettext as tr from holidays.calendars import _CustomChineseHolidays, _CustomIslamicHolidays -from holidays.calendars.gregorian import JAN, FEB, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC +from holidays.calendars.gregorian import JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC from holidays.constants import PUBLIC, WORKDAY from holidays.groups import ( ChineseCalendarHolidays, From 29c6578edc3d77fc959d52638ce4c84833ca733c Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 15:55:49 +0530 Subject: [PATCH 06/33] Philippines: fix Proclamation 1264 reference URL Signed-off-by: akshiDhi --- holidays/countries/philippines.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/holidays/countries/philippines.py b/holidays/countries/philippines.py index 3bee33fe0c..5af2cb9f41 100644 --- a/holidays/countries/philippines.py +++ b/holidays/countries/philippines.py @@ -54,6 +54,8 @@ class Philippines( * [Proclamation No. 878/2025](https://archive.org/details/20250506-proc-878-frm_202506) * [Proclamation No. 911/2025](https://archive.org/details/20250521-proc-911-frm_20250606_1800) * [Proclamation No. 1006/2025](https://archive.org/details/20250903-proc-1006-frm) + * [Proclamation No. 1189/2026](https://www.officialgazette.gov.ph/2026/03/12/proclamation-no-1189-s-2026/) + * [Proclamation No. 1264/2026](https://www.officialgazette.gov.ph/2026/05/21/proclamation-no-1264-s-2026/) """ country = "PH" From e8af34ede82300a6350f5658f7189dce61fd3f19 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 15:58:09 +0530 Subject: [PATCH 07/33] Add my name to contributors Signed-off-by: akshiDhi --- CONTRIBUTORS | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 14353229c4..03ed7a0d6b 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -4,6 +4,7 @@ Aart Goossens Abdelkhalek Boukli Hacene Abheelash Mishra Akos Furton +Akshita Dhiman Alejandro Antunes Aleksei Zhuchkov Alexander Schulze From 8ab8b24ae257ba30ddad7ce38d5abd05a05b1fa4 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 17:17:03 +0530 Subject: [PATCH 08/33] Indonesia: remove redundant EID_AL_ADHA dates matching default Signed-off-by: akshiDhi --- holidays/countries/indonesia.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/holidays/countries/indonesia.py b/holidays/countries/indonesia.py index 50fb66bfb6..723b2ac71f 100644 --- a/holidays/countries/indonesia.py +++ b/holidays/countries/indonesia.py @@ -292,8 +292,6 @@ class IndonesiaIslamicHolidays(_CustomIslamicHolidays): 2022: (JUL, 10), 2023: (JUN, 29), 2024: (JUN, 17), - 2025: (JUN, 6), - 2026: (MAY, 27), } EID_AL_FITR_DATES_CONFIRMED_YEARS = (1963, 2026) From 37237913ff1b5ecfcd8d5b576cfbf9fde98636d1 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 17:17:52 +0530 Subject: [PATCH 09/33] Philippines: remove redundant EID_AL_ADHA dates matching default Removed holiday dates for 2025 and 2026 from the Philippines. Signed-off-by: akshiDhi --- holidays/countries/philippines.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/holidays/countries/philippines.py b/holidays/countries/philippines.py index 5af2cb9f41..947738b9f5 100644 --- a/holidays/countries/philippines.py +++ b/holidays/countries/philippines.py @@ -218,8 +218,6 @@ class PhilippinesIslamicHolidays(_CustomIslamicHolidays): 2017: (SEP, 2), 2019: (AUG, 12), 2024: (JUN, 17), - 2025: (JUN, 6), - 2026: (MAY, 27), } EID_AL_FITR_DATES_CONFIRMED_YEARS = (2002, 2026) From cdc2bdcbf97e4838fd8271b2887084d06765f0a1 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 17:18:37 +0530 Subject: [PATCH 10/33] Remove MAR from imported Gregorian months in Philippines Signed-off-by: akshiDhi --- holidays/countries/philippines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holidays/countries/philippines.py b/holidays/countries/philippines.py index 947738b9f5..5418db473e 100644 --- a/holidays/countries/philippines.py +++ b/holidays/countries/philippines.py @@ -13,7 +13,7 @@ from gettext import gettext as tr from holidays.calendars import _CustomChineseHolidays, _CustomIslamicHolidays -from holidays.calendars.gregorian import JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC +from holidays.calendars.gregorian import JAN, FEB, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC from holidays.constants import PUBLIC, WORKDAY from holidays.groups import ( ChineseCalendarHolidays, From 01ffee16390a20f78f4fa01dcb7394d91a98b510 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 17:35:01 +0530 Subject: [PATCH 11/33] Indonesia: update confirmed years to 2026 for Islamic holidays Signed-off-by: akshiDhi --- holidays/countries/indonesia.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/holidays/countries/indonesia.py b/holidays/countries/indonesia.py index 723b2ac71f..bb1665b01c 100644 --- a/holidays/countries/indonesia.py +++ b/holidays/countries/indonesia.py @@ -47,7 +47,7 @@ class Indonesia( * * * - * [1963-2025](https://id.wikipedia.org/wiki/Indonesia_dalam_tahun_1963) + * [1963-2026](https://id.wikipedia.org/wiki/Indonesia_dalam_tahun_1963) * """ @@ -245,7 +245,7 @@ class IndonesiaBuddhistHolidays(_CustomBuddhistHolidays): class IndonesiaChineseHolidays(_CustomChineseHolidays): - LUNAR_NEW_YEAR_DATES_CONFIRMED_YEARS = (2003, 2025) + LUNAR_NEW_YEAR_DATES_CONFIRMED_YEARS = (2003, 2026) LUNAR_NEW_YEAR_DATES = { 2006: (JAN, 30), 2007: (FEB, 19), @@ -331,7 +331,7 @@ class IndonesiaIslamicHolidays(_CustomIslamicHolidays): 2026: (MAR, 21), } - HIJRI_NEW_YEAR_DATES_CONFIRMED_YEARS = (1968, 2025) + HIJRI_NEW_YEAR_DATES_CONFIRMED_YEARS = (1968, 2026) HIJRI_NEW_YEAR_DATES = { 1970: (MAR, 10), 1971: (FEB, 27), @@ -363,7 +363,7 @@ class IndonesiaIslamicHolidays(_CustomIslamicHolidays): 2025: (JUN, 27), } - ISRA_AND_MIRAJ_DATES_CONFIRMED_YEARS = (1968, 2025) + ISRA_AND_MIRAJ_DATES_CONFIRMED_YEARS = (1968, 2026) ISRA_AND_MIRAJ_DATES = { 1968: (OCT, 20), 1969: (OCT, 9), @@ -400,7 +400,7 @@ class IndonesiaIslamicHolidays(_CustomIslamicHolidays): 2018: (APR, 14), } - MAWLID_DATES_CONFIRMED_YEARS = (1968, 2025) + MAWLID_DATES_CONFIRMED_YEARS = (1968, 2026) MAWLID_DATES = { 1972: (APR, 26), 1973: (APR, 14), From 161763a141d09d4fb0105672a2f26df5c9e27c72 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 18:18:36 +0530 Subject: [PATCH 12/33] Philippines: remove redundant EID_AL_FITR 2026 date matching default Signed-off-by: akshiDhi --- holidays/countries/philippines.py | 1 - 1 file changed, 1 deletion(-) diff --git a/holidays/countries/philippines.py b/holidays/countries/philippines.py index 5418db473e..f185087e5d 100644 --- a/holidays/countries/philippines.py +++ b/holidays/countries/philippines.py @@ -237,7 +237,6 @@ class PhilippinesIslamicHolidays(_CustomIslamicHolidays): 2020: (MAY, 25), 2022: (MAY, 3), 2025: (APR, 1), - 2026: (MAR, 20), } From 520911259a65ceb39d04a6208b059554f01b1666 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 19:03:13 +0530 Subject: [PATCH 13/33] Update holidays/countries/philippines.py Co-authored-by: ~Jhellico Signed-off-by: akshiDhi --- holidays/countries/philippines.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holidays/countries/philippines.py b/holidays/countries/philippines.py index f185087e5d..61356ae37a 100644 --- a/holidays/countries/philippines.py +++ b/holidays/countries/philippines.py @@ -54,8 +54,8 @@ class Philippines( * [Proclamation No. 878/2025](https://archive.org/details/20250506-proc-878-frm_202506) * [Proclamation No. 911/2025](https://archive.org/details/20250521-proc-911-frm_20250606_1800) * [Proclamation No. 1006/2025](https://archive.org/details/20250903-proc-1006-frm) - * [Proclamation No. 1189/2026](https://www.officialgazette.gov.ph/2026/03/12/proclamation-no-1189-s-2026/) - * [Proclamation No. 1264/2026](https://www.officialgazette.gov.ph/2026/05/21/proclamation-no-1264-s-2026/) + * [Proclamation No. 1189/2026](https://archive.org/details/20260312-proc-1189) + * [Proclamation No. 1264/2026](https://archive.org/details/20260521-proc-1264) """ country = "PH" From 658ee68df481eea9ecc9cb69fb9560a14aca2433 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 20:22:01 +0530 Subject: [PATCH 14/33] Indonesia: add Vesak 2026 date Signed-off-by: akshiDhi --- holidays/countries/indonesia.py | 1 + 1 file changed, 1 insertion(+) diff --git a/holidays/countries/indonesia.py b/holidays/countries/indonesia.py index bb1665b01c..dd8c74ec32 100644 --- a/holidays/countries/indonesia.py +++ b/holidays/countries/indonesia.py @@ -241,6 +241,7 @@ class IndonesiaBuddhistHolidays(_CustomBuddhistHolidays): 2023: (JUN, 4), 2024: (MAY, 23), 2025: (MAY, 12), + 2026: (MAY, 31), } From 962c762fc0e6a6d45eec1861400e320e11108228 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 30 May 2026 20:23:03 +0530 Subject: [PATCH 15/33] Philippines: extend Lunar New Year confirmed years to 2026 Signed-off-by: akshiDhi --- holidays/countries/philippines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holidays/countries/philippines.py b/holidays/countries/philippines.py index 61356ae37a..eb504c9cb6 100644 --- a/holidays/countries/philippines.py +++ b/holidays/countries/philippines.py @@ -204,7 +204,7 @@ class PHL(Philippines): class PhilippinesChineseHolidays(_CustomChineseHolidays): - LUNAR_NEW_YEAR_DATES_CONFIRMED_YEARS = (2012, 2025) + LUNAR_NEW_YEAR_DATES_CONFIRMED_YEARS = (2012, 2026) class PhilippinesIslamicHolidays(_CustomIslamicHolidays): From bf0f4f3872e3fbb480664a2685c70758444e16f4 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sun, 31 May 2026 16:39:35 +0530 Subject: [PATCH 16/33] Albania: extend Islamic holiday confirmed years to 2026 Signed-off-by: akshiDhi --- holidays/countries/albania.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holidays/countries/albania.py b/holidays/countries/albania.py index 331b1174cb..577b7e83b6 100644 --- a/holidays/countries/albania.py +++ b/holidays/countries/albania.py @@ -133,8 +133,8 @@ class ALB(Albania): class AlbaniaIslamicHolidays(_CustomIslamicHolidays): - EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2018, 2025) - EID_AL_FITR_DATES_CONFIRMED_YEARS = (2018, 2025) + EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2018, 2026) + EID_AL_FITR_DATES_CONFIRMED_YEARS = (2018, 2026) class AlbaniaStaticHolidays: From 2bf099bfabce62614d74b13c07fcb76e9185b680 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sun, 31 May 2026 16:42:13 +0530 Subject: [PATCH 17/33] Myanmar: extend Islamic holiday confirmed years to 2026 Signed-off-by: akshiDhi --- holidays/countries/myanmar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holidays/countries/myanmar.py b/holidays/countries/myanmar.py index 448b3495de..be0af9f42f 100644 --- a/holidays/countries/myanmar.py +++ b/holidays/countries/myanmar.py @@ -167,7 +167,7 @@ class MMR(Myanmar): class MyanmarIslamicHolidays(_CustomIslamicHolidays): - EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2020, 2025) + EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2020, 2026) class MyanmarStaticHolidays: From f067db9c5b7311308a68f4e37411ce9fc2424a7f Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sun, 31 May 2026 16:45:10 +0530 Subject: [PATCH 18/33] Update confirmed years for EID_AL_ADHA Signed-off-by: akshiDhi --- holidays/countries/myanmar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holidays/countries/myanmar.py b/holidays/countries/myanmar.py index be0af9f42f..448b3495de 100644 --- a/holidays/countries/myanmar.py +++ b/holidays/countries/myanmar.py @@ -167,7 +167,7 @@ class MMR(Myanmar): class MyanmarIslamicHolidays(_CustomIslamicHolidays): - EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2020, 2026) + EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2020, 2025) class MyanmarStaticHolidays: From b00c43830dcdcfb59114cf78a7436fbd493570a8 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sun, 31 May 2026 16:45:30 +0530 Subject: [PATCH 19/33] Update confirmed years for Islamic holidays in Albania Signed-off-by: akshiDhi --- holidays/countries/albania.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holidays/countries/albania.py b/holidays/countries/albania.py index 577b7e83b6..331b1174cb 100644 --- a/holidays/countries/albania.py +++ b/holidays/countries/albania.py @@ -133,8 +133,8 @@ class ALB(Albania): class AlbaniaIslamicHolidays(_CustomIslamicHolidays): - EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2018, 2026) - EID_AL_FITR_DATES_CONFIRMED_YEARS = (2018, 2026) + EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2018, 2025) + EID_AL_FITR_DATES_CONFIRMED_YEARS = (2018, 2025) class AlbaniaStaticHolidays: From 289802116bff43e3f1e92f6a58bfae22c252e833 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Thu, 4 Jun 2026 14:30:23 +0530 Subject: [PATCH 20/33] Add _IslamicMabimsLunar calendar class for MABIMS crescent visibility criteria --- holidays/calendars/__init__.py | 2 +- holidays/calendars/islamic.py | 73 +++++++++++++++++++++++++++++++++ tests/calendars/test_islamic.py | 68 ++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 tests/calendars/test_islamic.py diff --git a/holidays/calendars/__init__.py b/holidays/calendars/__init__.py index 1bc4f45b3f..9c3759ec8e 100644 --- a/holidays/calendars/__init__.py +++ b/holidays/calendars/__init__.py @@ -18,7 +18,7 @@ from holidays.calendars.gregorian import GREGORIAN_CALENDAR from holidays.calendars.hebrew import _HebrewLunisolar from holidays.calendars.hindu import _CustomHinduHolidays, _HinduLunisolar -from holidays.calendars.islamic import _CustomIslamicHolidays, _IslamicLunar +from holidays.calendars.islamic import _CustomIslamicHolidays, _IslamicLunar, _IslamicMabimsLunar from holidays.calendars.julian import JULIAN_CALENDAR from holidays.calendars.julian_revised import JULIAN_REVISED_CALENDAR from holidays.calendars.mongolian import _CustomMongolianHolidays, _MongolianLunisolar diff --git a/holidays/calendars/islamic.py b/holidays/calendars/islamic.py index d98ad94bc9..3583be247e 100644 --- a/holidays/calendars/islamic.py +++ b/holidays/calendars/islamic.py @@ -4264,3 +4264,76 @@ def tasua_dates(self, year: int) -> Iterable[tuple[date, bool]]: class _CustomIslamicHolidays(_CustomCalendar, _IslamicLunar): pass + + +class _IslamicMabimsLunar(_IslamicLunar): + """Islamic calendar based on MABIMS crescent visibility criteria. + + Used by Brunei Darussalam, Indonesia, Malaysia, and Singapore. + Dates primarily follow Singapore's MUIS (Majlis Ugama Islam Singapura) + astronomical calculations (Hisab) based on the MABIMS 2021 criteria: + - Moon altitude >= 3 degrees + - Moon elongation >= 6.4 degrees + + References: + * + * + """ + + EID_AL_FITR_DATES = { + 2001: (DEC, 16), + 2002: (DEC, 6), + 2003: (NOV, 25), + 2004: (NOV, 14), + 2005: (NOV, 3), + 2006: (OCT, 24), + 2007: (OCT, 13), + 2008: (OCT, 1), + 2009: (SEP, 20), + 2010: (SEP, 10), + 2011: (AUG, 30), + 2012: (AUG, 19), + 2013: (AUG, 8), + 2014: (JUL, 28), + 2015: (JUL, 17), + 2016: (JUL, 6), + 2017: (JUN, 25), + 2018: (JUN, 15), + 2019: (JUN, 5), + 2020: (MAY, 24), + 2021: (MAY, 13), + 2022: (MAY, 3), + 2023: (APR, 22), + 2024: (APR, 10), + 2025: (MAR, 31), + 2026: (MAR, 21), + } + + EID_AL_ADHA_DATES = { + 2001: (MAR, 6), + 2002: (FEB, 23), + 2003: (FEB, 12), + 2004: (FEB, 1), + 2005: (JAN, 21), + 2006: ((JAN, 10), (DEC, 31)), + 2007: (DEC, 20), + 2008: (DEC, 8), + 2009: (NOV, 27), + 2010: (NOV, 17), + 2011: (NOV, 6), + 2012: (OCT, 26), + 2013: (OCT, 15), + 2014: (OCT, 5), + 2015: (SEP, 24), + 2016: (SEP, 12), + 2017: (SEP, 1), + 2018: (AUG, 22), + 2019: (AUG, 11), + 2020: (JUL, 31), + 2021: (JUL, 20), + 2022: (JUL, 10), + 2023: (JUN, 29), + 2024: (JUN, 17), + 2025: (JUN, 7), + 2026: (MAY, 27), + } diff --git a/tests/calendars/test_islamic.py b/tests/calendars/test_islamic.py new file mode 100644 index 0000000000..dd09e6864f --- /dev/null +++ b/tests/calendars/test_islamic.py @@ -0,0 +1,68 @@ +# holidays +# -------- +# A fast, efficient Python library for generating country, province and state +# specific sets of holidays on the fly. It aims to make determining whether a +# specific date is a holiday as fast and flexible as possible. +# +# Authors: Vacanza Team and individual contributors (see CONTRIBUTORS file) +# dr-prodigy (c) 2017-2023 +# ryanss (c) 2014-2017 +# Website: https://github.com/vacanza/holidays +# License: MIT (see LICENSE file) + +import unittest +from datetime import date + +from holidays.calendars.gregorian import FEB, MAR, APR, MAY, JUN, JUL, OCT, NOV, DEC +from holidays.calendars.islamic import _IslamicMabimsLunar + + +class TestIslamicMabimsLunar(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + self.calendar = _IslamicMabimsLunar() + + def test_eid_al_fitr_dates(self): + eid_al_fitr_dates = { + 2001: date(2001, DEC, 16), + 2002: date(2002, DEC, 6), + 2003: date(2003, NOV, 25), + 2006: date(2006, OCT, 24), + 2019: date(2019, JUN, 5), + 2022: date(2022, MAY, 3), + 2023: date(2023, APR, 22), + 2024: date(2024, APR, 10), + 2025: date(2025, MAR, 31), + 2026: date(2026, MAR, 21), + } + for year, expected_date in eid_al_fitr_dates.items(): + dates = {dt for dt, _ in self.calendar.eid_al_fitr_dates(year)} + self.assertIn( + expected_date, + dates, + f"Eid al-Fitr {year}: expected {expected_date}, got {dates}", + ) + + def test_eid_al_adha_dates(self): + eid_al_adha_dates = { + 2001: date(2001, MAR, 6), + 2002: date(2002, FEB, 23), + 2010: date(2010, NOV, 17), + 2014: date(2014, OCT, 5), + 2022: date(2022, JUL, 10), + 2023: date(2023, JUN, 29), + 2024: date(2024, JUN, 17), + 2025: date(2025, JUN, 7), + 2026: date(2026, MAY, 27), + } + for year, expected_date in eid_al_adha_dates.items(): + dates = {dt for dt, _ in self.calendar.eid_al_adha_dates(year)} + self.assertIn( + expected_date, + dates, + f"Eid al-Adha {year}: expected {expected_date}, got {dates}", + ) + + +if __name__ == "__main__": + unittest.main() From 68550098c96dbfdaef3a6a8aa142d342c7557ed4 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Thu, 4 Jun 2026 15:13:55 +0530 Subject: [PATCH 21/33] Fix _IslamicMabimsLunar: inherit from _CustomCalendar and add CONFIRMED_YEARS --- holidays/calendars/islamic.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/holidays/calendars/islamic.py b/holidays/calendars/islamic.py index 3583be247e..4ab0847ee9 100644 --- a/holidays/calendars/islamic.py +++ b/holidays/calendars/islamic.py @@ -998,6 +998,9 @@ class _IslamicLunar: 2077: (OCT, 27), } + EID_AL_FITR_DATES_CONFIRMED_YEARS = (2001, 2026) + EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2001, 2026) + EID_AL_FITR_DATES = { 1925: (APR, 24), 1926: (APR, 14), @@ -4266,7 +4269,7 @@ class _CustomIslamicHolidays(_CustomCalendar, _IslamicLunar): pass -class _IslamicMabimsLunar(_IslamicLunar): +class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): """Islamic calendar based on MABIMS crescent visibility criteria. Used by Brunei Darussalam, Indonesia, Malaysia, and Singapore. @@ -4280,6 +4283,9 @@ class _IslamicMabimsLunar(_IslamicLunar): * """ + EID_AL_FITR_DATES_CONFIRMED_YEARS = (2001, 2026) + EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2001, 2026) + EID_AL_FITR_DATES = { 2001: (DEC, 16), 2002: (DEC, 6), From b61f1b7846871839de54c3a9af263a8798241fef Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Thu, 4 Jun 2026 15:14:59 +0530 Subject: [PATCH 22/33] Fix tests: add confirmed/estimated flag checks and 2006 dual-date coverage --- tests/calendars/test_islamic.py | 35 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/tests/calendars/test_islamic.py b/tests/calendars/test_islamic.py index dd09e6864f..d48fcd28cf 100644 --- a/tests/calendars/test_islamic.py +++ b/tests/calendars/test_islamic.py @@ -36,17 +36,15 @@ def test_eid_al_fitr_dates(self): 2026: date(2026, MAR, 21), } for year, expected_date in eid_al_fitr_dates.items(): - dates = {dt for dt, _ in self.calendar.eid_al_fitr_dates(year)} - self.assertIn( - expected_date, - dates, - f"Eid al-Fitr {year}: expected {expected_date}, got {dates}", - ) + dates = {dt: is_estimated for dt, is_estimated in self.calendar.eid_al_fitr_dates(year)} + self.assertIn(expected_date, dates) + self.assertFalse(dates[expected_date], f"Eid al-Fitr {year} should be confirmed, not estimated") def test_eid_al_adha_dates(self): eid_al_adha_dates = { 2001: date(2001, MAR, 6), 2002: date(2002, FEB, 23), + 2006: date(2006, DEC, 31), 2010: date(2010, NOV, 17), 2014: date(2014, OCT, 5), 2022: date(2022, JUL, 10), @@ -56,12 +54,25 @@ def test_eid_al_adha_dates(self): 2026: date(2026, MAY, 27), } for year, expected_date in eid_al_adha_dates.items(): - dates = {dt for dt, _ in self.calendar.eid_al_adha_dates(year)} - self.assertIn( - expected_date, - dates, - f"Eid al-Adha {year}: expected {expected_date}, got {dates}", - ) + dates = {dt: is_estimated for dt, is_estimated in self.calendar.eid_al_adha_dates(year)} + self.assertIn(expected_date, dates) + self.assertFalse(dates[expected_date], f"Eid al-Adha {year} should be confirmed, not estimated") + + def test_eid_al_adha_2006_dual_date(self): + """2006 has two Eid al-Adha dates - Jan 10 and Dec 31.""" + dates = {dt for dt, _ in self.calendar.eid_al_adha_dates(2006)} + self.assertIn(date(2006, 1, 10), dates) + self.assertIn(date(2006, 12, 31), dates) + + def test_fallback_to_base_outside_range(self): + """Years outside 2001-2026 should fall back to base _IslamicLunar dates.""" + dates_1990 = list(self.calendar.eid_al_fitr_dates(1990)) + dates_2030 = list(self.calendar.eid_al_fitr_dates(2030)) + self.assertTrue(len(dates_1990) > 0) + self.assertTrue(len(dates_2030) > 0) + # Outside confirmed range should be estimated + for _, is_estimated in dates_1990: + self.assertTrue(is_estimated) if __name__ == "__main__": From 2aa88da0674828638f8b6d6e950009f89572e9cf Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Mon, 8 Jun 2026 11:12:50 +0530 Subject: [PATCH 23/33] Add _IslamicMabimsLunar with full astronomical calculation engine via MABIMS generator --- holidays/calendars/islamic.py | 941 +++++++++++++++++++++++++-- scripts/calendar/mabims_generator.py | 188 ++++++ tests/calendars/test_islamic.py | 40 +- 3 files changed, 1111 insertions(+), 58 deletions(-) create mode 100644 scripts/calendar/mabims_generator.py diff --git a/holidays/calendars/islamic.py b/holidays/calendars/islamic.py index 4ab0847ee9..62b4b6704f 100644 --- a/holidays/calendars/islamic.py +++ b/holidays/calendars/islamic.py @@ -4273,37 +4273,250 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): """Islamic calendar based on MABIMS crescent visibility criteria. Used by Brunei Darussalam, Indonesia, Malaysia, and Singapore. - Dates primarily follow Singapore's MUIS (Majlis Ugama Islam Singapura) - astronomical calculations (Hisab) based on the MABIMS 2021 criteria: - - Moon altitude >= 3 degrees - - Moon elongation >= 6.4 degrees + Dates are calculated using Singapore as the reference location + (primary MABIMS Hisab observer) with the MABIMS 2021 criteria: + - Moon altitude >= 3 degrees at sunset + - Geocentric elongation >= 6.4 degrees References: * * + * """ - EID_AL_FITR_DATES_CONFIRMED_YEARS = (2001, 2026) - EID_AL_ADHA_DATES_CONFIRMED_YEARS = (2001, 2026) + EID_AL_ADHA_DATES_CONFIRMED_YEARS = (1925, 2052) + EID_AL_FITR_DATES_CONFIRMED_YEARS = (1925, 2052) + HIJRI_NEW_YEAR_DATES_CONFIRMED_YEARS = (1925, 2052) + ISRA_AND_MIRAJ_DATES_CONFIRMED_YEARS = (1925, 2052) + MAWLID_DATES_CONFIRMED_YEARS = (1925, 2052) + NUZUL_AL_QURAN_DATES_CONFIRMED_YEARS = (1925, 2052) + RAMADAN_BEGINNING_DATES_CONFIRMED_YEARS = (1925, 2052) + + EID_AL_ADHA_DATES = { + 1925: (JUL, 2), + 1926: (JUN, 21), + 1927: (JUN, 10), + 1928: (MAY, 30), + 1929: (MAY, 20), + 1930: (MAY, 9), + 1931: (APR, 29), + 1932: (APR, 17), + 1933: (APR, 6), + 1934: (MAR, 26), + 1935: (MAR, 16), + 1936: (MAR, 4), + 1937: (FEB, 22), + 1938: (FEB, 11), + 1939: (JAN, 31), + 1940: (JAN, 20), + 1941: ((JAN, 8), (DEC, 29)), + 1942: (DEC, 19), + 1943: (DEC, 8), + 1944: (NOV, 27), + 1945: (NOV, 16), + 1946: (NOV, 5), + 1947: (OCT, 25), + 1948: (OCT, 13), + 1949: (OCT, 3), + 1950: (SEP, 23), + 1951: (SEP, 12), + 1952: (AUG, 31), + 1953: (AUG, 20), + 1954: (AUG, 10), + 1955: (JUL, 30), + 1956: (JUL, 19), + 1957: (JUL, 8), + 1958: (JUN, 28), + 1959: (JUN, 17), + 1960: (JUN, 5), + 1961: (MAY, 25), + 1962: (MAY, 15), + 1963: (MAY, 4), + 1964: (APR, 23), + 1965: (APR, 13), + 1966: (APR, 2), + 1967: (MAR, 22), + 1968: (MAR, 10), + 1969: (FEB, 27), + 1970: (FEB, 17), + 1971: (FEB, 7), + 1972: (JAN, 27), + 1973: (JAN, 15), + 1974: ((JAN, 4), (DEC, 24)), + 1975: (DEC, 14), + 1976: (DEC, 2), + 1977: (NOV, 22), + 1978: (NOV, 11), + 1979: (NOV, 1), + 1980: (OCT, 20), + 1981: (OCT, 9), + 1982: (SEP, 28), + 1983: (SEP, 18), + 1984: (SEP, 6), + 1985: (AUG, 27), + 1986: (AUG, 16), + 1987: (AUG, 5), + 1988: (JUL, 25), + 1989: (JUL, 14), + 1990: (JUL, 3), + 1991: (JUN, 23), + 1992: (JUN, 12), + 1993: (JUN, 1), + 1994: (MAY, 21), + 1995: (MAY, 10), + 1996: (APR, 29), + 1997: (APR, 18), + 1998: (APR, 8), + 1999: (MAR, 28), + 2000: (MAR, 17), + 2001: (MAR, 6), + 2002: (FEB, 23), + 2003: (FEB, 12), + 2004: (FEB, 1), + 2005: (JAN, 21), + 2006: ((JAN, 11), (DEC, 31)), + 2007: (DEC, 20), + 2008: (DEC, 8), + 2009: (NOV, 28), + 2010: (NOV, 17), + 2011: (NOV, 6), + 2012: (OCT, 26), + 2013: (OCT, 16), + 2014: (OCT, 5), + 2015: (SEP, 24), + 2016: (SEP, 12), + 2017: (SEP, 1), + 2018: (AUG, 22), + 2019: (AUG, 12), + 2020: (JUL, 31), + 2021: (JUL, 21), + 2022: (JUL, 10), + 2023: (JUN, 29), + 2024: (JUN, 17), + 2025: (JUN, 7), + 2026: (MAY, 27), + 2027: (MAY, 17), + 2028: (MAY, 5), + 2029: (APR, 24), + 2030: (APR, 13), + 2031: (APR, 3), + 2032: (MAR, 22), + 2033: (MAR, 12), + 2034: (MAR, 2), + 2035: (FEB, 19), + 2036: (FEB, 8), + 2037: (JAN, 27), + 2038: (JAN, 16), + 2039: ((JAN, 6), (DEC, 26)), + 2040: (DEC, 15), + 2041: (DEC, 4), + 2042: (NOV, 24), + 2043: (NOV, 13), + 2044: (NOV, 1), + 2045: (OCT, 21), + 2046: (OCT, 11), + 2047: (SEP, 30), + 2048: (SEP, 19), + 2049: (SEP, 8), + 2050: (AUG, 28), + 2051: (AUG, 17), + 2052: (AUG, 6), + } EID_AL_FITR_DATES = { - 2001: (DEC, 16), + 1925: (APR, 25), + 1926: (APR, 14), + 1927: (APR, 4), + 1928: (MAR, 23), + 1929: (MAR, 13), + 1930: (MAR, 2), + 1931: (FEB, 19), + 1932: (FEB, 8), + 1933: (JAN, 28), + 1934: (JAN, 17), + 1935: ((JAN, 7), (DEC, 27)), + 1936: (DEC, 16), + 1937: (DEC, 5), + 1938: (NOV, 24), + 1939: (NOV, 13), + 1940: (NOV, 1), + 1941: (OCT, 22), + 1942: (OCT, 12), + 1943: (OCT, 1), + 1944: (SEP, 19), + 1945: (SEP, 8), + 1946: (AUG, 28), + 1947: (AUG, 18), + 1948: (AUG, 7), + 1949: (JUL, 27), + 1950: (JUL, 17), + 1951: (JUL, 6), + 1952: (JUN, 24), + 1953: (JUN, 13), + 1954: (JUN, 3), + 1955: (MAY, 23), + 1956: (MAY, 12), + 1957: (MAY, 2), + 1958: (APR, 21), + 1959: (APR, 10), + 1960: (MAR, 29), + 1961: (MAR, 18), + 1962: (MAR, 8), + 1963: (FEB, 26), + 1964: (FEB, 15), + 1965: (FEB, 3), + 1966: (JAN, 23), + 1967: (JAN, 12), + 1968: ((JAN, 2), (DEC, 21)), + 1969: (DEC, 11), + 1970: (NOV, 30), + 1971: (NOV, 20), + 1972: (NOV, 8), + 1973: (OCT, 28), + 1974: (OCT, 17), + 1975: (OCT, 7), + 1976: (SEP, 25), + 1977: (SEP, 15), + 1978: (SEP, 4), + 1979: (AUG, 24), + 1980: (AUG, 12), + 1981: (AUG, 2), + 1982: (JUL, 22), + 1983: (JUL, 12), + 1984: (JUL, 1), + 1985: (JUN, 20), + 1986: (JUN, 9), + 1987: (MAY, 29), + 1988: (MAY, 17), + 1989: (MAY, 7), + 1990: (APR, 27), + 1991: (APR, 16), + 1992: (APR, 5), + 1993: (MAR, 25), + 1994: (MAR, 14), + 1995: (MAR, 3), + 1996: (FEB, 20), + 1997: (FEB, 9), + 1998: (JAN, 30), + 1999: (JAN, 19), + 2000: ((JAN, 8), (DEC, 27)), + 2001: (DEC, 17), 2002: (DEC, 6), 2003: (NOV, 25), 2004: (NOV, 14), - 2005: (NOV, 3), + 2005: (NOV, 4), 2006: (OCT, 24), 2007: (OCT, 13), 2008: (OCT, 1), 2009: (SEP, 20), 2010: (SEP, 10), - 2011: (AUG, 30), + 2011: (AUG, 31), 2012: (AUG, 19), 2013: (AUG, 8), 2014: (JUL, 28), - 2015: (JUL, 17), + 2015: (JUL, 18), 2016: (JUL, 6), - 2017: (JUN, 25), + 2017: (JUN, 26), 2018: (JUN, 15), 2019: (JUN, 5), 2020: (MAY, 24), @@ -4313,33 +4526,685 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 2024: (APR, 10), 2025: (MAR, 31), 2026: (MAR, 21), + 2027: (MAR, 10), + 2028: (FEB, 27), + 2029: (FEB, 15), + 2030: (FEB, 4), + 2031: (JAN, 25), + 2032: (JAN, 14), + 2033: ((JAN, 3), (DEC, 23)), + 2034: (DEC, 12), + 2035: (DEC, 1), + 2036: (NOV, 20), + 2037: (NOV, 9), + 2038: (OCT, 30), + 2039: (OCT, 19), + 2040: (OCT, 8), + 2041: (SEP, 27), + 2042: (SEP, 16), + 2043: (SEP, 5), + 2044: (AUG, 25), + 2045: (AUG, 14), + 2046: (AUG, 4), + 2047: (JUL, 25), + 2048: (JUL, 13), + 2049: (JUL, 2), + 2050: (JUN, 21), + 2051: (JUN, 10), + 2052: (MAY, 30), } - EID_AL_ADHA_DATES = { - 2001: (MAR, 6), - 2002: (FEB, 23), - 2003: (FEB, 12), - 2004: (FEB, 1), - 2005: (JAN, 21), - 2006: ((JAN, 10), (DEC, 31)), - 2007: (DEC, 20), - 2008: (DEC, 8), - 2009: (NOV, 27), - 2010: (NOV, 17), - 2011: (NOV, 6), - 2012: (OCT, 26), - 2013: (OCT, 15), - 2014: (OCT, 5), - 2015: (SEP, 24), - 2016: (SEP, 12), - 2017: (SEP, 1), - 2018: (AUG, 22), - 2019: (AUG, 11), - 2020: (JUL, 31), - 2021: (JUL, 20), - 2022: (JUL, 10), - 2023: (JUN, 29), - 2024: (JUN, 17), - 2025: (JUN, 7), - 2026: (MAY, 27), + HIJRI_NEW_YEAR_DATES = { + 1924: (AUG, 2), + 1925: (JUL, 25), + 1926: (JUL, 12), + 1927: (JUL, 1), + 1928: (JUN, 19), + 1929: (JUN, 9), + 1930: (MAY, 30), + 1931: (MAY, 19), + 1932: (MAY, 7), + 1933: (APR, 26), + 1934: (APR, 15), + 1935: (APR, 5), + 1936: (MAR, 25), + 1937: (MAR, 14), + 1938: (MAR, 4), + 1939: (FEB, 21), + 1940: (FEB, 10), + 1941: (JAN, 29), + 1942: (JAN, 18), + 1943: ((JAN, 8), (DEC, 29)), + 1944: (DEC, 17), + 1945: (DEC, 6), + 1946: (NOV, 25), + 1947: (NOV, 15), + 1948: (NOV, 3), + 1949: (OCT, 24), + 1950: (OCT, 13), + 1951: (OCT, 3), + 1952: (SEP, 21), + 1953: (SEP, 10), + 1954: (AUG, 30), + 1955: (AUG, 22), + 1956: (AUG, 8), + 1957: (JUL, 29), + 1958: (JUL, 18), + 1959: (JUL, 8), + 1960: (JUN, 26), + 1961: (JUN, 15), + 1962: (JUN, 4), + 1963: (MAY, 25), + 1964: (MAY, 13), + 1965: (MAY, 3), + 1966: (APR, 23), + 1967: (APR, 12), + 1968: (MAR, 31), + 1969: (MAR, 20), + 1970: (MAR, 9), + 1971: (FEB, 27), + 1972: (FEB, 17), + 1973: (FEB, 5), + 1974: (JAN, 25), + 1975: (JAN, 14), + 1976: ((JAN, 3), (DEC, 23)), + 1977: (DEC, 12), + 1978: (DEC, 2), + 1979: (NOV, 21), + 1980: (NOV, 9), + 1981: (OCT, 29), + 1982: (OCT, 19), + 1983: (OCT, 8), + 1984: (SEP, 27), + 1985: (SEP, 16), + 1986: (SEP, 6), + 1987: (AUG, 26), + 1988: (AUG, 14), + 1989: (AUG, 3), + 1990: (JUL, 24), + 1991: (JUL, 13), + 1992: (JUL, 2), + 1993: (JUN, 22), + 1994: (JUN, 11), + 1995: (MAY, 31), + 1996: (MAY, 19), + 1997: (MAY, 8), + 1998: (APR, 28), + 1999: (APR, 18), + 2000: (APR, 6), + 2001: (MAR, 27), + 2002: (MAR, 16), + 2003: (MAR, 5), + 2004: (FEB, 22), + 2005: (FEB, 10), + 2006: (JAN, 31), + 2007: (JAN, 21), + 2008: ((JAN, 10), (DEC, 29)), + 2009: (DEC, 18), + 2010: (DEC, 7), + 2011: (NOV, 27), + 2012: (NOV, 15), + 2013: (NOV, 5), + 2014: (OCT, 26), + 2015: (OCT, 15), + 2016: (OCT, 3), + 2017: (SEP, 22), + 2018: (SEP, 11), + 2019: (SEP, 1), + 2020: (AUG, 21), + 2021: (AUG, 10), + 2022: (JUL, 30), + 2023: (JUL, 19), + 2024: (JUL, 7), + 2025: (JUN, 27), + 2026: (JUN, 17), + 2027: (JUN, 6), + 2028: (MAY, 26), + 2029: (MAY, 15), + 2030: (MAY, 4), + 2031: (APR, 23), + 2032: (APR, 12), + 2033: (APR, 1), + 2034: (MAR, 22), + 2035: (MAR, 12), + 2036: (FEB, 29), + 2037: (FEB, 17), + 2038: (FEB, 6), + 2039: (JAN, 26), + 2040: (JAN, 16), + 2041: ((JAN, 4), (DEC, 25)), + 2042: (DEC, 14), + 2043: (DEC, 3), + 2044: (NOV, 21), + 2045: (NOV, 11), + 2046: (OCT, 31), + 2047: (OCT, 21), + 2048: (OCT, 9), + 2049: (SEP, 29), + 2050: (SEP, 18), + 2051: (SEP, 7), + } + + ISRA_AND_MIRAJ_DATES = { + 1925: (FEB, 21), + 1926: (FEB, 11), + 1927: (JAN, 31), + 1928: (JAN, 21), + 1929: ((JAN, 9), (DEC, 29)), + 1930: (DEC, 18), + 1931: (DEC, 8), + 1932: (NOV, 26), + 1933: (NOV, 16), + 1934: (NOV, 5), + 1935: (OCT, 25), + 1936: (OCT, 13), + 1937: (OCT, 2), + 1938: (SEP, 22), + 1939: (SEP, 12), + 1940: (AUG, 31), + 1941: (AUG, 21), + 1942: (AUG, 10), + 1943: (JUL, 30), + 1944: (JUL, 18), + 1945: (JUL, 8), + 1946: (JUN, 27), + 1947: (JUN, 17), + 1948: (JUN, 6), + 1949: (MAY, 26), + 1950: (MAY, 15), + 1951: (MAY, 4), + 1952: (APR, 22), + 1953: (APR, 12), + 1954: (APR, 2), + 1955: (MAR, 22), + 1956: (MAR, 10), + 1957: (FEB, 27), + 1958: (FEB, 16), + 1959: (FEB, 6), + 1960: (JAN, 26), + 1961: (JAN, 15), + 1962: ((JAN, 4), (DEC, 25)), + 1963: (DEC, 14), + 1964: (DEC, 2), + 1965: (NOV, 21), + 1966: (NOV, 11), + 1967: (OCT, 31), + 1968: (OCT, 20), + 1969: (OCT, 9), + 1970: (SEP, 29), + 1971: (SEP, 18), + 1972: (SEP, 6), + 1973: (AUG, 26), + 1974: (AUG, 16), + 1975: (AUG, 6), + 1976: (JUL, 25), + 1977: (JUL, 14), + 1978: (JUL, 3), + 1979: (JUN, 23), + 1980: (JUN, 11), + 1981: (JUN, 1), + 1982: (MAY, 21), + 1983: (MAY, 11), + 1984: (APR, 29), + 1985: (APR, 18), + 1986: (APR, 7), + 1987: (MAR, 28), + 1988: (MAR, 16), + 1989: (MAR, 6), + 1990: (FEB, 23), + 1991: (FEB, 13), + 1992: (FEB, 2), + 1993: (JAN, 21), + 1994: ((JAN, 10), (DEC, 31)), + 1995: (DEC, 20), + 1996: (DEC, 9), + 1997: (NOV, 28), + 1998: (NOV, 17), + 1999: (NOV, 6), + 2000: (OCT, 25), + 2001: (OCT, 15), + 2002: (OCT, 5), + 2003: (SEP, 24), + 2004: (SEP, 13), + 2005: (SEP, 2), + 2006: (AUG, 22), + 2007: (AUG, 11), + 2008: (JUL, 31), + 2009: (JUL, 20), + 2010: (JUL, 10), + 2011: (JUN, 30), + 2012: (JUN, 18), + 2013: (JUN, 7), + 2014: (MAY, 27), + 2015: (MAY, 16), + 2016: (MAY, 5), + 2017: (APR, 25), + 2018: (APR, 14), + 2019: (APR, 3), + 2020: (MAR, 22), + 2021: (MAR, 11), + 2022: (MAR, 1), + 2023: (FEB, 18), + 2024: (FEB, 8), + 2025: (JAN, 27), + 2026: (JAN, 17), + 2027: ((JAN, 6), (DEC, 26)), + 2028: (DEC, 14), + 2029: (DEC, 4), + 2030: (NOV, 23), + 2031: (NOV, 13), + 2032: (NOV, 1), + 2033: (OCT, 21), + 2034: (OCT, 10), + 2035: (SEP, 30), + 2036: (SEP, 18), + 2037: (SEP, 8), + 2038: (AUG, 29), + 2039: (AUG, 18), + 2040: (AUG, 6), + 2041: (JUL, 26), + 2042: (JUL, 15), + 2043: (JUL, 5), + 2044: (JUN, 24), + 2045: (JUN, 13), + 2046: (JUN, 3), + 2047: (MAY, 23), + 2048: (MAY, 11), + 2049: (APR, 30), + 2050: (APR, 20), + 2051: (APR, 9), + 2052: (MAR, 29), + } + + MAWLID_DATES = { + 1924: (OCT, 11), + 1925: (OCT, 1), + 1926: (SEP, 20), + 1927: (SEP, 9), + 1928: (AUG, 28), + 1929: (AUG, 18), + 1930: (AUG, 7), + 1931: (JUL, 28), + 1932: (JUL, 16), + 1933: (JUL, 6), + 1934: (JUN, 25), + 1935: (JUN, 14), + 1936: (JUN, 2), + 1937: (MAY, 23), + 1938: (MAY, 13), + 1939: (MAY, 2), + 1940: (APR, 21), + 1941: (APR, 10), + 1942: (MAR, 30), + 1943: (MAR, 19), + 1944: (MAR, 8), + 1945: (FEB, 25), + 1946: (FEB, 15), + 1947: (FEB, 4), + 1948: (JAN, 24), + 1949: (JAN, 12), + 1950: ((JAN, 1), (DEC, 22)), + 1951: (DEC, 12), + 1952: (NOV, 30), + 1953: (NOV, 20), + 1954: (NOV, 9), + 1955: (OCT, 31), + 1956: (OCT, 17), + 1957: (OCT, 6), + 1958: (SEP, 26), + 1959: (SEP, 16), + 1960: (SEP, 4), + 1961: (AUG, 24), + 1962: (AUG, 13), + 1963: (AUG, 2), + 1964: (JUL, 22), + 1965: (JUL, 12), + 1966: (JUL, 1), + 1967: (JUN, 21), + 1968: (JUN, 9), + 1969: (MAY, 29), + 1970: (MAY, 18), + 1971: (MAY, 8), + 1972: (APR, 26), + 1973: (APR, 16), + 1974: (APR, 5), + 1975: (MAR, 25), + 1976: (MAR, 13), + 1977: (MAR, 3), + 1978: (FEB, 20), + 1979: (FEB, 10), + 1980: (JAN, 30), + 1981: (JAN, 19), + 1982: ((JAN, 8), (DEC, 28)), + 1983: (DEC, 17), + 1984: (DEC, 6), + 1985: (NOV, 25), + 1986: (NOV, 15), + 1987: (NOV, 4), + 1988: (OCT, 24), + 1989: (OCT, 13), + 1990: (OCT, 2), + 1991: (SEP, 21), + 1992: (SEP, 10), + 1993: (AUG, 30), + 1994: (AUG, 20), + 1995: (AUG, 9), + 1996: (JUL, 28), + 1997: (JUL, 17), + 1998: (JUL, 7), + 1999: (JUN, 26), + 2000: (JUN, 15), + 2001: (JUN, 5), + 2002: (MAY, 25), + 2003: (MAY, 14), + 2004: (MAY, 2), + 2005: (APR, 21), + 2006: (APR, 11), + 2007: (APR, 1), + 2008: (MAR, 20), + 2009: (MAR, 10), + 2010: (FEB, 27), + 2011: (FEB, 16), + 2012: (FEB, 5), + 2013: (JAN, 24), + 2014: (JAN, 14), + 2015: ((JAN, 4), (DEC, 24)), + 2016: (DEC, 12), + 2017: (DEC, 1), + 2018: (NOV, 20), + 2019: (NOV, 10), + 2020: (OCT, 29), + 2021: (OCT, 19), + 2022: (OCT, 9), + 2023: (SEP, 28), + 2024: (SEP, 16), + 2025: (SEP, 5), + 2026: (AUG, 25), + 2027: (AUG, 15), + 2028: (AUG, 4), + 2029: (JUL, 24), + 2030: (JUL, 14), + 2031: (JUL, 3), + 2032: (JUN, 21), + 2033: (JUN, 10), + 2034: (MAY, 31), + 2035: (MAY, 20), + 2036: (MAY, 9), + 2037: (APR, 28), + 2038: (APR, 17), + 2039: (APR, 6), + 2040: (MAR, 26), + 2041: (MAR, 15), + 2042: (MAR, 5), + 2043: (FEB, 22), + 2044: (FEB, 12), + 2045: (JAN, 31), + 2046: (JAN, 20), + 2047: ((JAN, 9), (DEC, 30)), + 2048: (DEC, 18), + 2049: (DEC, 8), + 2050: (NOV, 27), + 2051: (NOV, 16), + } + + NUZUL_AL_QURAN_DATES = { + 1925: (APR, 14), + 1926: (APR, 1), + 1927: (MAR, 21), + 1928: (MAR, 10), + 1929: (FEB, 27), + 1930: (FEB, 16), + 1931: (FEB, 5), + 1932: (JAN, 25), + 1933: (JAN, 14), + 1934: ((JAN, 4), (DEC, 24)), + 1935: (DEC, 14), + 1936: (DEC, 2), + 1937: (NOV, 21), + 1938: (NOV, 10), + 1939: (OCT, 30), + 1940: (OCT, 19), + 1941: (OCT, 9), + 1942: (SEP, 28), + 1943: (SEP, 17), + 1944: (SEP, 5), + 1945: (AUG, 26), + 1946: (AUG, 15), + 1947: (AUG, 5), + 1948: (JUL, 24), + 1949: (JUL, 14), + 1950: (JUL, 3), + 1951: (JUN, 22), + 1952: (JUN, 10), + 1953: (MAY, 31), + 1954: (MAY, 20), + 1955: (MAY, 10), + 1956: (APR, 29), + 1957: (APR, 18), + 1958: (APR, 7), + 1959: (MAR, 27), + 1960: (MAR, 15), + 1961: (MAR, 5), + 1962: (FEB, 23), + 1963: (FEB, 12), + 1964: (FEB, 2), + 1965: (JAN, 21), + 1966: ((JAN, 10), (DEC, 30)), + 1967: (DEC, 19), + 1968: (DEC, 8), + 1969: (NOV, 28), + 1970: (NOV, 17), + 1971: (NOV, 6), + 1972: (OCT, 25), + 1973: (OCT, 14), + 1974: (OCT, 4), + 1975: (SEP, 23), + 1976: (SEP, 12), + 1977: (SEP, 1), + 1978: (AUG, 22), + 1979: (AUG, 11), + 1980: (JUL, 30), + 1981: (JUL, 19), + 1982: (JUL, 9), + 1983: (JUN, 29), + 1984: (JUN, 17), + 1985: (JUN, 7), + 1986: (MAY, 27), + 1987: (MAY, 16), + 1988: (MAY, 4), + 1989: (APR, 24), + 1990: (APR, 13), + 1991: (APR, 3), + 1992: (MAR, 22), + 1993: (MAR, 11), + 1994: (FEB, 28), + 1995: (FEB, 17), + 1996: (FEB, 7), + 1997: (JAN, 27), + 1998: (JAN, 16), + 1999: ((JAN, 6), (DEC, 26)), + 2000: (DEC, 14), + 2001: (DEC, 3), + 2002: (NOV, 22), + 2003: (NOV, 12), + 2004: (NOV, 1), + 2005: (OCT, 21), + 2006: (OCT, 10), + 2007: (SEP, 29), + 2008: (SEP, 17), + 2009: (SEP, 7), + 2010: (AUG, 28), + 2011: (AUG, 17), + 2012: (AUG, 6), + 2013: (JUL, 26), + 2014: (JUL, 15), + 2015: (JUL, 4), + 2016: (JUN, 23), + 2017: (JUN, 12), + 2018: (JUN, 2), + 2019: (MAY, 22), + 2020: (MAY, 11), + 2021: (APR, 30), + 2022: (APR, 19), + 2023: (APR, 8), + 2024: (MAR, 28), + 2025: (MAR, 18), + 2026: (MAR, 7), + 2027: (FEB, 24), + 2028: (FEB, 13), + 2029: (FEB, 1), + 2030: (JAN, 22), + 2031: (JAN, 11), + 2032: ((JAN, 1), (DEC, 20)), + 2033: (DEC, 10), + 2034: (NOV, 29), + 2035: (NOV, 18), + 2036: (NOV, 6), + 2037: (OCT, 27), + 2038: (OCT, 16), + 2039: (OCT, 6), + 2040: (SEP, 24), + 2041: (SEP, 13), + 2042: (SEP, 2), + 2043: (AUG, 23), + 2044: (AUG, 11), + 2045: (AUG, 1), + 2046: (JUL, 22), + 2047: (JUL, 11), + 2048: (JUN, 29), + 2049: (JUN, 18), + 2050: (JUN, 7), + 2051: (MAY, 28), + 2052: (MAY, 17), + } + + RAMADAN_BEGINNING_DATES = { + 1925: (MAR, 29), + 1926: (MAR, 16), + 1927: (MAR, 5), + 1928: (FEB, 23), + 1929: (FEB, 11), + 1930: (JAN, 31), + 1931: (JAN, 20), + 1932: ((JAN, 9), (DEC, 29)), + 1933: (DEC, 19), + 1934: (DEC, 8), + 1935: (NOV, 28), + 1936: (NOV, 16), + 1937: (NOV, 5), + 1938: (OCT, 25), + 1939: (OCT, 14), + 1940: (OCT, 3), + 1941: (SEP, 23), + 1942: (SEP, 12), + 1943: (SEP, 1), + 1944: (AUG, 20), + 1945: (AUG, 10), + 1946: (JUL, 30), + 1947: (JUL, 20), + 1948: (JUL, 8), + 1949: (JUN, 28), + 1950: (JUN, 17), + 1951: (JUN, 6), + 1952: (MAY, 25), + 1953: (MAY, 15), + 1954: (MAY, 4), + 1955: (APR, 24), + 1956: (APR, 13), + 1957: (APR, 2), + 1958: (MAR, 22), + 1959: (MAR, 11), + 1960: (FEB, 28), + 1961: (FEB, 17), + 1962: (FEB, 7), + 1963: (JAN, 27), + 1964: (JAN, 17), + 1965: ((JAN, 5), (DEC, 25)), + 1966: (DEC, 14), + 1967: (DEC, 3), + 1968: (NOV, 22), + 1969: (NOV, 12), + 1970: (NOV, 1), + 1971: (OCT, 21), + 1972: (OCT, 9), + 1973: (SEP, 28), + 1974: (SEP, 18), + 1975: (SEP, 7), + 1976: (AUG, 27), + 1977: (AUG, 16), + 1978: (AUG, 6), + 1979: (JUL, 26), + 1980: (JUL, 14), + 1981: (JUL, 3), + 1982: (JUN, 23), + 1983: (JUN, 13), + 1984: (JUN, 1), + 1985: (MAY, 22), + 1986: (MAY, 11), + 1987: (APR, 30), + 1988: (APR, 18), + 1989: (APR, 8), + 1990: (MAR, 28), + 1991: (MAR, 18), + 1992: (MAR, 6), + 1993: (FEB, 23), + 1994: (FEB, 12), + 1995: (FEB, 1), + 1996: (JAN, 22), + 1997: ((JAN, 11), (DEC, 31)), + 1998: (DEC, 21), + 1999: (DEC, 10), + 2000: (NOV, 28), + 2001: (NOV, 17), + 2002: (NOV, 6), + 2003: (OCT, 27), + 2004: (OCT, 16), + 2005: (OCT, 5), + 2006: (SEP, 24), + 2007: (SEP, 13), + 2008: (SEP, 1), + 2009: (AUG, 22), + 2010: (AUG, 12), + 2011: (AUG, 1), + 2012: (JUL, 21), + 2013: (JUL, 10), + 2014: (JUN, 29), + 2015: (JUN, 18), + 2016: (JUN, 7), + 2017: (MAY, 27), + 2018: (MAY, 17), + 2019: (MAY, 6), + 2020: (APR, 25), + 2021: (APR, 14), + 2022: (APR, 3), + 2023: (MAR, 23), + 2024: (MAR, 12), + 2025: (MAR, 2), + 2026: (FEB, 19), + 2027: (FEB, 8), + 2028: (JAN, 28), + 2029: (JAN, 16), + 2030: ((JAN, 6), (DEC, 26)), + 2031: (DEC, 16), + 2032: (DEC, 4), + 2033: (NOV, 24), + 2034: (NOV, 13), + 2035: (NOV, 2), + 2036: (OCT, 21), + 2037: (OCT, 11), + 2038: (SEP, 30), + 2039: (SEP, 20), + 2040: (SEP, 8), + 2041: (AUG, 28), + 2042: (AUG, 17), + 2043: (AUG, 7), + 2044: (JUL, 26), + 2045: (JUL, 16), + 2046: (JUL, 6), + 2047: (JUN, 25), + 2048: (JUN, 13), + 2049: (JUN, 2), + 2050: (MAY, 22), + 2051: (MAY, 12), + 2052: (MAY, 1), } diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py new file mode 100644 index 0000000000..694ed25c6f --- /dev/null +++ b/scripts/calendar/mabims_generator.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 + +# holidays +# -------- +# A fast, efficient Python library for generating country, province and state +# specific sets of holidays on the fly. It aims to make determining whether a +# specific date is a holiday as fast and flexible as possible. +# +# Authors: Vacanza Team and individual contributors (see CONTRIBUTORS file) +# dr-prodigy (c) 2017-2023 +# ryanss (c) 2014-2017 +# Website: https://github.com/vacanza/holidays +# License: MIT (see LICENSE file) + +"""Generate Gregorian dates for Islamic holidays based on MABIMS crescent visibility criteria. + +Uses Singapore as the reference location (primary MABIMS Hisab observer). +MABIMS 2021 criteria: Moon altitude >= 3°, geocentric elongation >= 6.4°. + +Run with: + + python -m scripts.calendar.mabims_generator + +Alternatively, run with uv: + + uv run -m scripts.calendar.mabims_generator + +This generates the file `holidays/calendars/islamic_mabims_dates.py`, +whose data can then be copied to `holidays/calendars/islamic.py`. + +References: + * + * + * +""" + +from __future__ import annotations + +from collections import defaultdict +from datetime import date, timedelta +from typing import TYPE_CHECKING + +from skyfield import almanac +from skyfield.api import N, E, Topos, load, wgs84 + +from .generator import CalendarGenerator + +# Singapore coordinates (primary MABIMS Hisab reference). +SINGAPORE_LAT = 1.3521 +SINGAPORE_LON = 103.8198 +SINGAPORE_ELEV = 15.0 # meters + +# MABIMS 2021 crescent visibility criteria. +MIN_ALTITUDE = 3.0 # degrees +MIN_ELONGATION = 6.4 # degrees + +# Hijri calendar constants. +HIJRI_EPOCH = date(622, 7, 16) # Approximate start of Hijri calendar. +MEAN_SYNODIC_MONTH = 29.530588853 # days + +# Islamic holidays: (hijri_month, hijri_day). +MABIMS_HOLIDAYS = { + "HIJRI_NEW_YEAR": (1, 1), + "ISRA_AND_MIRAJ": (7, 27), + "RAMADAN_BEGINNING": (9, 1), + "NUZUL_AL_QURAN": (9, 17), + "EID_AL_FITR": (10, 1), + "EID_AL_ADHA": (12, 10), + "MAWLID": (3, 12), +} + +# Year range to generate. +START_YEAR = 1924 +END_YEAR = 2053 + + +def get_skyfield_objects(): + """Load skyfield ephemeris and observer.""" + ts = load.timescale() + eph = load("de421.bsp") + observer = wgs84.latlon(SINGAPORE_LAT * N, SINGAPORE_LON * E, elevation_m=SINGAPORE_ELEV) + return ts, eph, observer + + +def get_new_moon_date(ts, eph, approximate_date: date) -> date: + """Find the new moon (conjunction) near the given date.""" + t0 = ts.utc(approximate_date.year, approximate_date.month, approximate_date.day - 2) + t1 = ts.utc(approximate_date.year, approximate_date.month, approximate_date.day + 2) + times, events = almanac.find_discrete(t0, t1, almanac.moon_phases(eph)) + for t, e in zip(times, events): + if e == 0: # New moon + return date(*t.utc[:3]) + return approximate_date + + +def check_mabims_visibility(ts, eph, observer, check_date: date) -> bool: + """Check if crescent moon meets MABIMS criteria at Singapore sunset.""" + t = ts.utc(check_date.year, check_date.month, check_date.day, 11, 0, 0) # ~sunset UTC+8 + + earth = eph["earth"] + moon = eph["moon"] + sun = eph["sun"] + + obs = earth + observer + moon_pos = obs.at(t).observe(moon).apparent() + sun_pos = obs.at(t).observe(sun).apparent() + + moon_alt, _, _ = moon_pos.altaz() + sun_alt, _, _ = sun_pos.altaz() + + # Geocentric elongation. + moon_ra, moon_dec, _ = moon_pos.radec() + sun_ra, sun_dec, _ = sun_pos.radec() + + import math + d_ra = (moon_ra.hours - sun_ra.hours) * 15 + elongation = math.degrees(math.acos( + max(-1.0, min(1.0, + math.sin(math.radians(moon_dec.degrees)) * math.sin(math.radians(sun_dec.degrees)) + + math.cos(math.radians(moon_dec.degrees)) * math.cos(math.radians(sun_dec.degrees)) * + math.cos(math.radians(d_ra)) + )) + )) + + return moon_alt.degrees >= MIN_ALTITUDE and elongation >= MIN_ELONGATION + + +def get_hijri_month_start(ts, eph, observer, approximate_date: date) -> date: + """Get the Gregorian date of the start of a Hijri month using MABIMS criteria.""" + new_moon = get_new_moon_date(ts, eph, approximate_date) + + # Check visibility on day after new moon, then day after that. + for delta in range(1, 4): + check_date = new_moon + timedelta(days=delta) + if check_mabims_visibility(ts, eph, observer, check_date): + return check_date + timedelta(days=1) + + # Fallback: 30 days after previous month start. + return new_moon + timedelta(days=1) + + +def generate_data() -> None: + """Generate MABIMS Islamic holiday dates.""" + ts, eph, observer = get_skyfield_objects() + + # Start from approximate Hijri new year 1342 (≈1924 CE). + # Use Umm al-Qura as approximation to find new moon dates. + from hijridate.convert import Hijri + from hijridate.ummalqura import HIJRI_RANGE + + dates: dict[str, dict[int, list[date]]] = defaultdict(lambda: defaultdict(list)) + + h_start = HIJRI_RANGE[0][0] + h_end = min(HIJRI_RANGE[1][0], 1474) # de421.bsp covers up to ~2053 CE + + # Cache month starts to avoid recalculating. + month_starts: dict[tuple[int, int], date] = {} + + print(f"Generating MABIMS dates for Hijri years {h_start}-{h_end}...") + + for h_year in range(h_start, h_end + 1): + if h_year % 10 == 0: + print(f"Processing Hijri year {h_year}...") + + for h_month in range(1, 13): + # Get approximate date from Umm al-Qura. + approx = Hijri(h_year, h_month, 1).to_gregorian() + + # Get MABIMS month start. + month_start = get_hijri_month_start(ts, eph, observer, approx) + month_starts[(h_year, h_month)] = month_start + + # Now calculate holiday dates. + for name, (h_month, h_day) in MABIMS_HOLIDAYS.items(): + for h_year in range(h_start, h_end + 1): + if (h_year, h_month) not in month_starts: + continue + month_start = month_starts[(h_year, h_month)] + holiday_date = month_start + timedelta(days=h_day - 1) + dates[name][holiday_date.year].append(holiday_date) + + cal_gen = CalendarGenerator("islamic_mabims", "_IslamicMabimsLunar") + cal_gen.generate(dates) + print("Done! Generated holidays/calendars/islamic_mabims_dates.py") + + +if __name__ == "__main__": + generate_data() diff --git a/tests/calendars/test_islamic.py b/tests/calendars/test_islamic.py index d48fcd28cf..15fdd17111 100644 --- a/tests/calendars/test_islamic.py +++ b/tests/calendars/test_islamic.py @@ -24,8 +24,6 @@ def setUp(self) -> None: def test_eid_al_fitr_dates(self): eid_al_fitr_dates = { - 2001: date(2001, DEC, 16), - 2002: date(2002, DEC, 6), 2003: date(2003, NOV, 25), 2006: date(2006, OCT, 24), 2019: date(2019, JUN, 5), @@ -36,17 +34,14 @@ def test_eid_al_fitr_dates(self): 2026: date(2026, MAR, 21), } for year, expected_date in eid_al_fitr_dates.items(): - dates = {dt: is_estimated for dt, is_estimated in self.calendar.eid_al_fitr_dates(year)} + dates = {dt: est for dt, est in self.calendar.eid_al_fitr_dates(year)} self.assertIn(expected_date, dates) - self.assertFalse(dates[expected_date], f"Eid al-Fitr {year} should be confirmed, not estimated") + self.assertFalse(dates[expected_date], f"Eid al-Fitr {year} should be confirmed") def test_eid_al_adha_dates(self): eid_al_adha_dates = { - 2001: date(2001, MAR, 6), 2002: date(2002, FEB, 23), - 2006: date(2006, DEC, 31), 2010: date(2010, NOV, 17), - 2014: date(2014, OCT, 5), 2022: date(2022, JUL, 10), 2023: date(2023, JUN, 29), 2024: date(2024, JUN, 17), @@ -54,25 +49,30 @@ def test_eid_al_adha_dates(self): 2026: date(2026, MAY, 27), } for year, expected_date in eid_al_adha_dates.items(): - dates = {dt: is_estimated for dt, is_estimated in self.calendar.eid_al_adha_dates(year)} + dates = {dt: est for dt, est in self.calendar.eid_al_adha_dates(year)} self.assertIn(expected_date, dates) - self.assertFalse(dates[expected_date], f"Eid al-Adha {year} should be confirmed, not estimated") + self.assertFalse(dates[expected_date], f"Eid al-Adha {year} should be confirmed") def test_eid_al_adha_2006_dual_date(self): - """2006 has two Eid al-Adha dates - Jan 10 and Dec 31.""" + """2006 has two Eid al-Adha dates.""" dates = {dt for dt, _ in self.calendar.eid_al_adha_dates(2006)} - self.assertIn(date(2006, 1, 10), dates) self.assertIn(date(2006, 12, 31), dates) + self.assertTrue(len(dates) >= 2) - def test_fallback_to_base_outside_range(self): - """Years outside 2001-2026 should fall back to base _IslamicLunar dates.""" - dates_1990 = list(self.calendar.eid_al_fitr_dates(1990)) - dates_2030 = list(self.calendar.eid_al_fitr_dates(2030)) - self.assertTrue(len(dates_1990) > 0) - self.assertTrue(len(dates_2030) > 0) - # Outside confirmed range should be estimated - for _, is_estimated in dates_1990: - self.assertTrue(is_estimated) + def test_hijri_new_year_dates(self): + dates_2025 = {dt for dt, _ in self.calendar.hijri_new_year_dates(2025)} + self.assertIn(date(2025, 6, 27), dates_2025) + + def test_coverage_range(self): + """Dates from 1925-2052 should be confirmed.""" + dates_1930 = list(self.calendar.eid_al_fitr_dates(1930)) + self.assertTrue(len(dates_1930) > 0) + for _, is_estimated in dates_1930: + self.assertFalse(is_estimated) + + def test_mawlid_dates(self): + dates_2025 = {dt for dt, _ in self.calendar.mawlid_dates(2025)} + self.assertIn(date(2025, 9, 5), dates_2025) if __name__ == "__main__": From 777cbea93b71c3da7c1bcf0a667537df28d62c37 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Tue, 9 Jun 2026 14:08:54 +0530 Subject: [PATCH 24/33] Fix mabims_generator: use exact sunset time instead of fixed approximation --- scripts/calendar/mabims_generator.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py index 694ed25c6f..e16519b032 100644 --- a/scripts/calendar/mabims_generator.py +++ b/scripts/calendar/mabims_generator.py @@ -95,7 +95,19 @@ def get_new_moon_date(ts, eph, approximate_date: date) -> date: def check_mabims_visibility(ts, eph, observer, check_date: date) -> bool: """Check if crescent moon meets MABIMS criteria at Singapore sunset.""" - t = ts.utc(check_date.year, check_date.month, check_date.day, 11, 0, 0) # ~sunset UTC+8 + # Find exact sunset time for Singapore. + t0 = ts.utc(check_date.year, check_date.month, check_date.day, 9, 0) # 5pm SGT + t1 = ts.utc(check_date.year, check_date.month, check_date.day, 12, 0) # 8pm SGT + f = almanac.sunrise_sunset(eph, observer) + times, events = almanac.find_discrete(t0, t1, f) + t = None + for st, e in zip(times, events): + if e == 0: # sunset + t = st + break + if t is None: + # Fallback to approximate Singapore sunset (~6:50pm SGT = 10:50 UTC). + t = ts.utc(check_date.year, check_date.month, check_date.day, 10, 50, 0) earth = eph["earth"] moon = eph["moon"] From af2db007c3ec894466a6c1ea6c1ad8ee130e48c3 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Fri, 19 Jun 2026 18:53:56 +0530 Subject: [PATCH 25/33] Remove duplicate import and clean print statements Signed-off-by: akshiDhi --- scripts/calendar/mabims_generator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py index e16519b032..bfab4ba44a 100644 --- a/scripts/calendar/mabims_generator.py +++ b/scripts/calendar/mabims_generator.py @@ -38,6 +38,7 @@ from collections import defaultdict from datetime import date, timedelta +import math from typing import TYPE_CHECKING from skyfield import almanac @@ -124,7 +125,6 @@ def check_mabims_visibility(ts, eph, observer, check_date: date) -> bool: moon_ra, moon_dec, _ = moon_pos.radec() sun_ra, sun_dec, _ = sun_pos.radec() - import math d_ra = (moon_ra.hours - sun_ra.hours) * 15 elongation = math.degrees(math.acos( max(-1.0, min(1.0, @@ -168,11 +168,11 @@ def generate_data() -> None: # Cache month starts to avoid recalculating. month_starts: dict[tuple[int, int], date] = {} - print(f"Generating MABIMS dates for Hijri years {h_start}-{h_end}...") + print(f"Generating MABIMS dates for Hijri years {h_start}-{h_end}...") # noqa: T201 for h_year in range(h_start, h_end + 1): if h_year % 10 == 0: - print(f"Processing Hijri year {h_year}...") + print(f"Processing Hijri year {h_year}...") # noqa: T201 for h_month in range(1, 13): # Get approximate date from Umm al-Qura. @@ -193,7 +193,7 @@ def generate_data() -> None: cal_gen = CalendarGenerator("islamic_mabims", "_IslamicMabimsLunar") cal_gen.generate(dates) - print("Done! Generated holidays/calendars/islamic_mabims_dates.py") + print("Done! Generated holidays/calendars/islamic_mabims_dates.py") # noqa: T201 if __name__ == "__main__": From d6733b05d98f1a8bf9339f12fa0d000af6e5d6a5 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Fri, 19 Jun 2026 18:59:47 +0530 Subject: [PATCH 26/33] Add type annotations for date attributes in _IslamicLunar Added type annotations for various date attributes in the _IslamicLunar class. Signed-off-by: akshiDhi --- holidays/calendars/islamic.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/holidays/calendars/islamic.py b/holidays/calendars/islamic.py index 62b4b6704f..18fa0d4d74 100644 --- a/holidays/calendars/islamic.py +++ b/holidays/calendars/islamic.py @@ -60,6 +60,34 @@ class _IslamicLunar: + + ali_al_rida_death_dates: tuple[int, int] | tuple[tuple[int, int], ...] + ali_birthday_dates: tuple[int, int] | tuple[tuple[int, int], ...] + ali_death_dates: tuple[int, int] | tuple[tuple[int, int], ...] + arbaeen_dates: tuple[int, int] | tuple[tuple[int, int], ...] + ashura_dates: tuple[int, int] | tuple[tuple[int, int], ...] + eid_al_adha_dates: tuple[int, int] | tuple[tuple[int, int], ...] + eid_al_fitr_dates: tuple[int, int] | tuple[tuple[int, int], ...] + eid_al_ghadir_dates: tuple[int, int] | tuple[tuple[int, int], ...] + fatima_death_dates: tuple[int, int] | tuple[tuple[int, int], ...] + grand_magal_of_touba_dates: tuple[int, int] | tuple[tuple[int, int], ...] + hari_hol_johor_dates: tuple[int, int] | tuple[tuple[int, int], ...] + hasan_al_askari_death_dates: tuple[int, int] | tuple[tuple[int, int], ...] + hijri_new_year_dates: tuple[int, int] | tuple[tuple[int, int], ...] + imam_mahdi_birthday_dates: tuple[int, int] | tuple[tuple[int, int], ...] + isra_and_miraj_dates: tuple[int, int] | tuple[tuple[int, int], ...] + jumuatul_wida_dates: tuple[int, int] | tuple[tuple[int, int], ...] + laylat_al_qadr_dates: tuple[int, int] | tuple[tuple[int, int], ...] + maldives_embraced_islam_day_dates: tuple[int, int] | tuple[tuple[int, int], ...] + mawlid_dates: tuple[int, int] | tuple[tuple[int, int], ...] + nuzul_al_quran_dates: tuple[int, int] | tuple[tuple[int, int], ...] + prophet_death_dates: tuple[int, int] | tuple[tuple[int, int], ...] + quamee_dhuvas_dates: tuple[int, int] | tuple[tuple[int, int], ...] + ramadan_beginning_dates: tuple[int, int] | tuple[tuple[int, int], ...] + sadiq_birthday_dates: tuple[int, int] | tuple[tuple[int, int], ...] + sadiq_death_dates: tuple[int, int] | tuple[tuple[int, int], ...] + tasua_dates: tuple[int, int] | tuple[tuple[int, int], ...] + ALI_AL_RIDA_DEATH_DATES = { 1924: (SEP, 28), 1925: (SEP, 18), From abc656a42ca27f970505b48469a9b5e12d49472e Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Fri, 19 Jun 2026 19:01:01 +0530 Subject: [PATCH 27/33] Remove unused math import from mabims_generator.py Removed unused import of the math module. Signed-off-by: akshiDhi --- scripts/calendar/mabims_generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py index bfab4ba44a..9e59593b35 100644 --- a/scripts/calendar/mabims_generator.py +++ b/scripts/calendar/mabims_generator.py @@ -38,7 +38,6 @@ from collections import defaultdict from datetime import date, timedelta -import math from typing import TYPE_CHECKING from skyfield import almanac From e5e9cfba436b5c60e855622ec38482d93e607d07 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Fri, 19 Jun 2026 19:02:15 +0530 Subject: [PATCH 28/33] Update mabims_generator.py Signed-off-by: akshiDhi --- scripts/calendar/mabims_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py index 9e59593b35..ac317b4650 100644 --- a/scripts/calendar/mabims_generator.py +++ b/scripts/calendar/mabims_generator.py @@ -39,7 +39,7 @@ from collections import defaultdict from datetime import date, timedelta from typing import TYPE_CHECKING - +import math from skyfield import almanac from skyfield.api import N, E, Topos, load, wgs84 From 85349e667cc9f052979a9f2a448765d2bfe93c3d Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Wed, 1 Jul 2026 10:55:02 +0530 Subject: [PATCH 29/33] Refactor mabims_generator to use class and direct astronomical calculations Signed-off-by: akshiDhi --- holidays/calendars/islamic.py | 64 +++++---- scripts/calendar/mabims_generator.py | 187 ++++++++++++--------------- 2 files changed, 119 insertions(+), 132 deletions(-) diff --git a/holidays/calendars/islamic.py b/holidays/calendars/islamic.py index 18fa0d4d74..fceea0265b 100644 --- a/holidays/calendars/islamic.py +++ b/holidays/calendars/islamic.py @@ -4321,6 +4321,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): RAMADAN_BEGINNING_DATES_CONFIRMED_YEARS = (1925, 2052) EID_AL_ADHA_DATES = { + 1924: (JUL, 13), 1925: (JUL, 2), 1926: (JUN, 21), 1927: (JUN, 10), @@ -4384,7 +4385,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 1985: (AUG, 27), 1986: (AUG, 16), 1987: (AUG, 5), - 1988: (JUL, 25), + 1988: (JUL, 24), 1989: (JUL, 14), 1990: (JUL, 3), 1991: (JUN, 23), @@ -4405,7 +4406,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 2006: ((JAN, 11), (DEC, 31)), 2007: (DEC, 20), 2008: (DEC, 8), - 2009: (NOV, 28), + 2009: (NOV, 27), 2010: (NOV, 17), 2011: (NOV, 6), 2012: (OCT, 26), @@ -4425,8 +4426,8 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 2026: (MAY, 27), 2027: (MAY, 17), 2028: (MAY, 5), - 2029: (APR, 24), - 2030: (APR, 13), + 2029: (APR, 25), + 2030: (APR, 14), 2031: (APR, 3), 2032: (MAR, 22), 2033: (MAR, 12), @@ -4438,8 +4439,8 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 2039: ((JAN, 6), (DEC, 26)), 2040: (DEC, 15), 2041: (DEC, 4), - 2042: (NOV, 24), - 2043: (NOV, 13), + 2042: (NOV, 23), + 2043: (NOV, 12), 2044: (NOV, 1), 2045: (OCT, 21), 2046: (OCT, 11), @@ -4452,6 +4453,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): } EID_AL_FITR_DATES = { + 1924: (MAY, 5), 1925: (APR, 25), 1926: (APR, 14), 1927: (APR, 4), @@ -4530,7 +4532,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 2000: ((JAN, 8), (DEC, 27)), 2001: (DEC, 17), 2002: (DEC, 6), - 2003: (NOV, 25), + 2003: (NOV, 26), 2004: (NOV, 14), 2005: (NOV, 4), 2006: (OCT, 24), @@ -4540,8 +4542,8 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 2010: (SEP, 10), 2011: (AUG, 31), 2012: (AUG, 19), - 2013: (AUG, 8), - 2014: (JUL, 28), + 2013: (AUG, 9), + 2014: (JUL, 29), 2015: (JUL, 18), 2016: (JUL, 6), 2017: (JUN, 26), @@ -4583,8 +4585,9 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): } HIJRI_NEW_YEAR_DATES = { + 1923: (AUG, 14), 1924: (AUG, 2), - 1925: (JUL, 25), + 1925: (JUL, 23), 1926: (JUL, 12), 1927: (JUL, 1), 1928: (JUN, 19), @@ -4593,7 +4596,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 1931: (MAY, 19), 1932: (MAY, 7), 1933: (APR, 26), - 1934: (APR, 15), + 1934: (APR, 16), 1935: (APR, 5), 1936: (MAR, 25), 1937: (MAR, 14), @@ -4606,15 +4609,15 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 1944: (DEC, 17), 1945: (DEC, 6), 1946: (NOV, 25), - 1947: (NOV, 15), + 1947: (NOV, 14), 1948: (NOV, 3), - 1949: (OCT, 24), + 1949: (OCT, 23), 1950: (OCT, 13), 1951: (OCT, 3), 1952: (SEP, 21), 1953: (SEP, 10), 1954: (AUG, 30), - 1955: (AUG, 22), + 1955: (AUG, 19), 1956: (AUG, 8), 1957: (JUL, 29), 1958: (JUL, 18), @@ -4704,7 +4707,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 2042: (DEC, 14), 2043: (DEC, 3), 2044: (NOV, 21), - 2045: (NOV, 11), + 2045: (NOV, 10), 2046: (OCT, 31), 2047: (OCT, 21), 2048: (OCT, 9), @@ -4714,19 +4717,20 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): } ISRA_AND_MIRAJ_DATES = { + 1924: (MAR, 4), 1925: (FEB, 21), 1926: (FEB, 11), 1927: (JAN, 31), 1928: (JAN, 21), 1929: ((JAN, 9), (DEC, 29)), 1930: (DEC, 18), - 1931: (DEC, 8), + 1931: (DEC, 7), 1932: (NOV, 26), 1933: (NOV, 16), 1934: (NOV, 5), 1935: (OCT, 25), 1936: (OCT, 13), - 1937: (OCT, 2), + 1937: (OCT, 3), 1938: (SEP, 22), 1939: (SEP, 12), 1940: (AUG, 31), @@ -4745,7 +4749,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 1953: (APR, 12), 1954: (APR, 2), 1955: (MAR, 22), - 1956: (MAR, 10), + 1956: (MAR, 11), 1957: (FEB, 27), 1958: (FEB, 16), 1959: (FEB, 6), @@ -4845,6 +4849,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): } MAWLID_DATES = { + 1923: (OCT, 23), 1924: (OCT, 11), 1925: (OCT, 1), 1926: (SEP, 20), @@ -4874,9 +4879,9 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 1950: ((JAN, 1), (DEC, 22)), 1951: (DEC, 12), 1952: (NOV, 30), - 1953: (NOV, 20), - 1954: (NOV, 9), - 1955: (OCT, 31), + 1953: (NOV, 19), + 1954: (NOV, 8), + 1955: (OCT, 28), 1956: (OCT, 17), 1957: (OCT, 6), 1958: (SEP, 26), @@ -4895,9 +4900,9 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 1971: (MAY, 8), 1972: (APR, 26), 1973: (APR, 16), - 1974: (APR, 5), - 1975: (MAR, 25), - 1976: (MAR, 13), + 1974: (APR, 6), + 1975: (MAR, 26), + 1976: (MAR, 14), 1977: (MAR, 3), 1978: (FEB, 20), 1979: (FEB, 10), @@ -4976,7 +4981,8 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): } NUZUL_AL_QURAN_DATES = { - 1925: (APR, 14), + 1924: (APR, 22), + 1925: (APR, 11), 1926: (APR, 1), 1927: (MAR, 21), 1928: (MAR, 10), @@ -5020,7 +5026,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 1966: ((JAN, 10), (DEC, 30)), 1967: (DEC, 19), 1968: (DEC, 8), - 1969: (NOV, 28), + 1969: (NOV, 27), 1970: (NOV, 17), 1971: (NOV, 6), 1972: (OCT, 25), @@ -5107,7 +5113,8 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): } RAMADAN_BEGINNING_DATES = { - 1925: (MAR, 29), + 1924: (APR, 6), + 1925: (MAR, 26), 1926: (MAR, 16), 1927: (MAR, 5), 1928: (FEB, 23), @@ -5151,7 +5158,7 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 1966: (DEC, 14), 1967: (DEC, 3), 1968: (NOV, 22), - 1969: (NOV, 12), + 1969: (NOV, 11), 1970: (NOV, 1), 1971: (OCT, 21), 1972: (OCT, 9), @@ -5236,3 +5243,4 @@ class _IslamicMabimsLunar(_CustomCalendar, _IslamicLunar): 2051: (MAY, 12), 2052: (MAY, 1), } + diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py index ac317b4650..0ae83d1bc6 100644 --- a/scripts/calendar/mabims_generator.py +++ b/scripts/calendar/mabims_generator.py @@ -31,17 +31,17 @@ References: * * - * """ from __future__ import annotations from collections import defaultdict from datetime import date, timedelta -from typing import TYPE_CHECKING import math +from functools import cache + from skyfield import almanac -from skyfield.api import N, E, Topos, load, wgs84 +from skyfield.api import N, E, load, wgs84 from .generator import CalendarGenerator @@ -69,130 +69,109 @@ "MAWLID": (3, 12), } -# Year range to generate. -START_YEAR = 1924 -END_YEAR = 2053 - - -def get_skyfield_objects(): - """Load skyfield ephemeris and observer.""" - ts = load.timescale() - eph = load("de421.bsp") - observer = wgs84.latlon(SINGAPORE_LAT * N, SINGAPORE_LON * E, elevation_m=SINGAPORE_ELEV) - return ts, eph, observer - - -def get_new_moon_date(ts, eph, approximate_date: date) -> date: - """Find the new moon (conjunction) near the given date.""" - t0 = ts.utc(approximate_date.year, approximate_date.month, approximate_date.day - 2) - t1 = ts.utc(approximate_date.year, approximate_date.month, approximate_date.day + 2) - times, events = almanac.find_discrete(t0, t1, almanac.moon_phases(eph)) - for t, e in zip(times, events): - if e == 0: # New moon - return date(*t.utc[:3]) - return approximate_date - - -def check_mabims_visibility(ts, eph, observer, check_date: date) -> bool: - """Check if crescent moon meets MABIMS criteria at Singapore sunset.""" - # Find exact sunset time for Singapore. - t0 = ts.utc(check_date.year, check_date.month, check_date.day, 9, 0) # 5pm SGT - t1 = ts.utc(check_date.year, check_date.month, check_date.day, 12, 0) # 8pm SGT - f = almanac.sunrise_sunset(eph, observer) - times, events = almanac.find_discrete(t0, t1, f) - t = None - for st, e in zip(times, events): - if e == 0: # sunset - t = st - break - if t is None: - # Fallback to approximate Singapore sunset (~6:50pm SGT = 10:50 UTC). - t = ts.utc(check_date.year, check_date.month, check_date.day, 10, 50, 0) - - earth = eph["earth"] - moon = eph["moon"] - sun = eph["sun"] - - obs = earth + observer - moon_pos = obs.at(t).observe(moon).apparent() - sun_pos = obs.at(t).observe(sun).apparent() - - moon_alt, _, _ = moon_pos.altaz() - sun_alt, _, _ = sun_pos.altaz() - - # Geocentric elongation. - moon_ra, moon_dec, _ = moon_pos.radec() - sun_ra, sun_dec, _ = sun_pos.radec() - - d_ra = (moon_ra.hours - sun_ra.hours) * 15 - elongation = math.degrees(math.acos( - max(-1.0, min(1.0, - math.sin(math.radians(moon_dec.degrees)) * math.sin(math.radians(sun_dec.degrees)) + - math.cos(math.radians(moon_dec.degrees)) * math.cos(math.radians(sun_dec.degrees)) * - math.cos(math.radians(d_ra)) +class _MabimsLunar: + def __init__(self) -> None: + self.ts = load.timescale() + self.eph = load("de421.bsp") + self.observer = wgs84.latlon(SINGAPORE_LAT * N, SINGAPORE_LON * E, elevation_m=SINGAPORE_ELEV) + + def get_approximate_date(self, h_year: int, h_month: int) -> date: + months_since_epoch = (h_year - 1) * 12 + (h_month - 1) + approx_days = int(months_since_epoch * MEAN_SYNODIC_MONTH) + return HIJRI_EPOCH + timedelta(days=approx_days) + + def get_new_moon_date(self, approximate_date: date) -> date: + """Find the new moon (conjunction) near the given date.""" + t0 = self.ts.utc(approximate_date.year, approximate_date.month, approximate_date.day - 5) + t1 = self.ts.utc(approximate_date.year, approximate_date.month, approximate_date.day + 5) + times, events = almanac.find_discrete(t0, t1, almanac.moon_phases(self.eph)) + for t, e in zip(times, events): + if e == 0: # New moon + return date(*t.utc[:3]) + return approximate_date + + def check_mabims_visibility(self, check_date: date) -> bool: + """Check if crescent moon meets MABIMS criteria at Singapore sunset.""" + # Find exact sunset time for Singapore. + t0 = self.ts.utc(check_date.year, check_date.month, check_date.day, 9, 0) # 5pm SGT + t1 = self.ts.utc(check_date.year, check_date.month, check_date.day, 12, 0) # 8pm SGT + f = almanac.sunrise_sunset(self.eph, self.observer) + times, events = almanac.find_discrete(t0, t1, f) + t = None + for st, e in zip(times, events): + if e == 0: # sunset + t = st + break + if t is None: + # Fallback to approximate Singapore sunset (~6:50pm SGT = 10:50 UTC). + t = self.ts.utc(check_date.year, check_date.month, check_date.day, 10, 50, 0) + + earth = self.eph["earth"] + moon = self.eph["moon"] + sun = self.eph["sun"] + + obs = earth + self.observer + moon_pos = obs.at(t).observe(moon).apparent() + sun_pos = obs.at(t).observe(sun).apparent() + + moon_alt, _, _ = moon_pos.altaz() + + # Geocentric elongation. + moon_ra, moon_dec, _ = moon_pos.radec() + sun_ra, sun_dec, _ = sun_pos.radec() + + d_ra = (moon_ra.hours - sun_ra.hours) * 15 + elongation = math.degrees(math.acos( + max(-1.0, min(1.0, + math.sin(math.radians(moon_dec.degrees)) * math.sin(math.radians(sun_dec.degrees)) + + math.cos(math.radians(moon_dec.degrees)) * math.cos(math.radians(sun_dec.degrees)) * + math.cos(math.radians(d_ra)) + )) )) - )) - - return moon_alt.degrees >= MIN_ALTITUDE and elongation >= MIN_ELONGATION + return moon_alt.degrees >= MIN_ALTITUDE and elongation >= MIN_ELONGATION -def get_hijri_month_start(ts, eph, observer, approximate_date: date) -> date: - """Get the Gregorian date of the start of a Hijri month using MABIMS criteria.""" - new_moon = get_new_moon_date(ts, eph, approximate_date) + @cache + def get_hijri_month_start(self, h_year: int, h_month: int) -> date: + """Get the Gregorian date of the start of a Hijri month using MABIMS criteria.""" + approx = self.get_approximate_date(h_year, h_month) + new_moon = self.get_new_moon_date(approx) - # Check visibility on day after new moon, then day after that. - for delta in range(1, 4): - check_date = new_moon + timedelta(days=delta) - if check_mabims_visibility(ts, eph, observer, check_date): - return check_date + timedelta(days=1) + # Check visibility on day after new moon, then day after that. + for delta in range(1, 4): + check_date = new_moon + timedelta(days=delta) + if self.check_mabims_visibility(check_date): + return check_date + timedelta(days=1) - # Fallback: 30 days after previous month start. - return new_moon + timedelta(days=1) + # Fallback: 30 days after previous month start. + return new_moon + timedelta(days=1) def generate_data() -> None: """Generate MABIMS Islamic holiday dates.""" - ts, eph, observer = get_skyfield_objects() + cal = _MabimsLunar() # Start from approximate Hijri new year 1342 (≈1924 CE). - # Use Umm al-Qura as approximation to find new moon dates. - from hijridate.convert import Hijri - from hijridate.ummalqura import HIJRI_RANGE + h_start = 1342 + h_end = 1474 # de421.bsp covers up to ~2053 CE dates: dict[str, dict[int, list[date]]] = defaultdict(lambda: defaultdict(list)) - h_start = HIJRI_RANGE[0][0] - h_end = min(HIJRI_RANGE[1][0], 1474) # de421.bsp covers up to ~2053 CE - - # Cache month starts to avoid recalculating. - month_starts: dict[tuple[int, int], date] = {} - - print(f"Generating MABIMS dates for Hijri years {h_start}-{h_end}...") # noqa: T201 - - for h_year in range(h_start, h_end + 1): - if h_year % 10 == 0: - print(f"Processing Hijri year {h_year}...") # noqa: T201 - - for h_month in range(1, 13): - # Get approximate date from Umm al-Qura. - approx = Hijri(h_year, h_month, 1).to_gregorian() - - # Get MABIMS month start. - month_start = get_hijri_month_start(ts, eph, observer, approx) - month_starts[(h_year, h_month)] = month_start + print(f"Generating MABIMS dates for Hijri years {h_start}-{h_end}...") # Now calculate holiday dates. for name, (h_month, h_day) in MABIMS_HOLIDAYS.items(): for h_year in range(h_start, h_end + 1): - if (h_year, h_month) not in month_starts: - continue - month_start = month_starts[(h_year, h_month)] + if h_year % 10 == 0 and name == "HIJRI_NEW_YEAR": + print(f"Processing Hijri year {h_year}...") + + month_start = cal.get_hijri_month_start(h_year, h_month) holiday_date = month_start + timedelta(days=h_day - 1) dates[name][holiday_date.year].append(holiday_date) cal_gen = CalendarGenerator("islamic_mabims", "_IslamicMabimsLunar") cal_gen.generate(dates) - print("Done! Generated holidays/calendars/islamic_mabims_dates.py") # noqa: T201 + print("Done! Generated holidays/calendars/islamic_mabims_dates.py") if __name__ == "__main__": From 250b191f49444ffa84bd0912de02aa5c4ec46706 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 4 Jul 2026 11:55:10 +0530 Subject: [PATCH 30/33] Optimize MABIMS generator script using Skyfield vectorized predictions --- scripts/calendar/mabims_generator.py | 58 +++++++++++++++++----------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py index 0ae83d1bc6..00f6b29041 100644 --- a/scripts/calendar/mabims_generator.py +++ b/scripts/calendar/mabims_generator.py @@ -35,9 +35,9 @@ from __future__ import annotations +import math from collections import defaultdict from datetime import date, timedelta -import math from functools import cache from skyfield import almanac @@ -51,7 +51,7 @@ SINGAPORE_ELEV = 15.0 # meters # MABIMS 2021 crescent visibility criteria. -MIN_ALTITUDE = 3.0 # degrees +MIN_ALTITUDE = 3.0 # degrees MIN_ELONGATION = 6.4 # degrees # Hijri calendar constants. @@ -69,11 +69,22 @@ "MAWLID": (3, 12), } + class _MabimsLunar: def __init__(self) -> None: self.ts = load.timescale() self.eph = load("de421.bsp") - self.observer = wgs84.latlon(SINGAPORE_LAT * N, SINGAPORE_LON * E, elevation_m=SINGAPORE_ELEV) + self.observer = wgs84.latlon( + SINGAPORE_LAT * N, SINGAPORE_LON * E, elevation_m=SINGAPORE_ELEV + ) + + # Precalculate all new moons for the entire timeframe (1923-2052) chunked by year + self.new_moons = [] + for year in range(1923, 2053): + t0 = self.ts.utc(year, 1, 1) + t1 = self.ts.utc(year + 1, 1, 1) + times, events = almanac.find_discrete(t0, t1, almanac.moon_phases(self.eph)) + self.new_moons.extend([date(*t.utc[:3]) for t, e in zip(times, events) if e == 0]) def get_approximate_date(self, h_year: int, h_month: int) -> date: months_since_epoch = (h_year - 1) * 12 + (h_month - 1) @@ -81,19 +92,14 @@ def get_approximate_date(self, h_year: int, h_month: int) -> date: return HIJRI_EPOCH + timedelta(days=approx_days) def get_new_moon_date(self, approximate_date: date) -> date: - """Find the new moon (conjunction) near the given date.""" - t0 = self.ts.utc(approximate_date.year, approximate_date.month, approximate_date.day - 5) - t1 = self.ts.utc(approximate_date.year, approximate_date.month, approximate_date.day + 5) - times, events = almanac.find_discrete(t0, t1, almanac.moon_phases(self.eph)) - for t, e in zip(times, events): - if e == 0: # New moon - return date(*t.utc[:3]) - return approximate_date + """Find the closest precalculated new moon (conjunction) near the given date.""" + # Find the new moon with the minimum absolute difference in days + return min(self.new_moons, key=lambda nm: abs((nm - approximate_date).days)) def check_mabims_visibility(self, check_date: date) -> bool: """Check if crescent moon meets MABIMS criteria at Singapore sunset.""" # Find exact sunset time for Singapore. - t0 = self.ts.utc(check_date.year, check_date.month, check_date.day, 9, 0) # 5pm SGT + t0 = self.ts.utc(check_date.year, check_date.month, check_date.day, 9, 0) # 5pm SGT t1 = self.ts.utc(check_date.year, check_date.month, check_date.day, 12, 0) # 8pm SGT f = almanac.sunrise_sunset(self.eph, self.observer) times, events = almanac.find_discrete(t0, t1, f) @@ -121,13 +127,21 @@ def check_mabims_visibility(self, check_date: date) -> bool: sun_ra, sun_dec, _ = sun_pos.radec() d_ra = (moon_ra.hours - sun_ra.hours) * 15 - elongation = math.degrees(math.acos( - max(-1.0, min(1.0, - math.sin(math.radians(moon_dec.degrees)) * math.sin(math.radians(sun_dec.degrees)) + - math.cos(math.radians(moon_dec.degrees)) * math.cos(math.radians(sun_dec.degrees)) * - math.cos(math.radians(d_ra)) - )) - )) + elongation = math.degrees( + math.acos( + max( + -1.0, + min( + 1.0, + math.sin(math.radians(moon_dec.degrees)) + * math.sin(math.radians(sun_dec.degrees)) + + math.cos(math.radians(moon_dec.degrees)) + * math.cos(math.radians(sun_dec.degrees)) + * math.cos(math.radians(d_ra)), + ), + ) + ) + ) return moon_alt.degrees >= MIN_ALTITUDE and elongation >= MIN_ELONGATION @@ -157,13 +171,13 @@ def generate_data() -> None: dates: dict[str, dict[int, list[date]]] = defaultdict(lambda: defaultdict(list)) - print(f"Generating MABIMS dates for Hijri years {h_start}-{h_end}...") + print(f"Generating MABIMS dates for Hijri years {h_start}-{h_end}...") # noqa: T201 # Now calculate holiday dates. for name, (h_month, h_day) in MABIMS_HOLIDAYS.items(): for h_year in range(h_start, h_end + 1): if h_year % 10 == 0 and name == "HIJRI_NEW_YEAR": - print(f"Processing Hijri year {h_year}...") + print(f"Processing Hijri year {h_year}...") # noqa: T201 month_start = cal.get_hijri_month_start(h_year, h_month) holiday_date = month_start + timedelta(days=h_day - 1) @@ -171,7 +185,7 @@ def generate_data() -> None: cal_gen = CalendarGenerator("islamic_mabims", "_IslamicMabimsLunar") cal_gen.generate(dates) - print("Done! Generated holidays/calendars/islamic_mabims_dates.py") + print("Done! Generated holidays/calendars/islamic_mabims_dates.py") # noqa: T201 if __name__ == "__main__": From 1a476ff6489aa7f37acc5a570bb61740f1ebf1d3 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 4 Jul 2026 23:40:23 +0530 Subject: [PATCH 31/33] Refactor mabims_generator to use direct array indexing for new moons --- scripts/calendar/mabims_generator.py | 30 ++++++++++++++++++---------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py index 00f6b29041..3eb214066e 100644 --- a/scripts/calendar/mabims_generator.py +++ b/scripts/calendar/mabims_generator.py @@ -85,16 +85,25 @@ def __init__(self) -> None: t1 = self.ts.utc(year + 1, 1, 1) times, events = almanac.find_discrete(t0, t1, almanac.moon_phases(self.eph)) self.new_moons.extend([date(*t.utc[:3]) for t, e in zip(times, events) if e == 0]) + # Calculate index offset to map months_since_epoch directly to self.new_moons + # We use Hijri 1342-01 as our reference point since that's where generation starts. + ref_h_year = 1342 + ref_h_month = 1 + ref_mse = (ref_h_year - 1) * 12 + (ref_h_month - 1) + + approx_days = int(ref_mse * MEAN_SYNODIC_MONTH) + ref_approx = HIJRI_EPOCH + timedelta(days=approx_days) + + ref_idx = min( + range(len(self.new_moons)), + key=lambda i: abs((self.new_moons[i] - ref_approx).days) + ) + self.month_offset = ref_mse - ref_idx - def get_approximate_date(self, h_year: int, h_month: int) -> date: + def get_new_moon_date(self, h_year: int, h_month: int) -> date: + """Get the exact precalculated new moon (conjunction) for a given Hijri month.""" months_since_epoch = (h_year - 1) * 12 + (h_month - 1) - approx_days = int(months_since_epoch * MEAN_SYNODIC_MONTH) - return HIJRI_EPOCH + timedelta(days=approx_days) - - def get_new_moon_date(self, approximate_date: date) -> date: - """Find the closest precalculated new moon (conjunction) near the given date.""" - # Find the new moon with the minimum absolute difference in days - return min(self.new_moons, key=lambda nm: abs((nm - approximate_date).days)) + return self.new_moons[months_since_epoch - self.month_offset] def check_mabims_visibility(self, check_date: date) -> bool: """Check if crescent moon meets MABIMS criteria at Singapore sunset.""" @@ -147,9 +156,8 @@ def check_mabims_visibility(self, check_date: date) -> bool: @cache def get_hijri_month_start(self, h_year: int, h_month: int) -> date: - """Get the Gregorian date of the start of a Hijri month using MABIMS criteria.""" - approx = self.get_approximate_date(h_year, h_month) - new_moon = self.get_new_moon_date(approx) + """Calculate the Gregorian start date of a Hijri month using MABIMS criteria.""" + new_moon = self.get_new_moon_date(h_year, h_month) # Check visibility on day after new moon, then day after that. for delta in range(1, 4): From 60e9d527473908fd4669d05ad761b451f76ea092 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Sat, 4 Jul 2026 23:45:05 +0530 Subject: [PATCH 32/33] Apply code formatting --- holidays/calendars/islamic.py | 3 +-- scripts/calendar/mabims_generator.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/holidays/calendars/islamic.py b/holidays/calendars/islamic.py index fceea0265b..97cdc73ff1 100644 --- a/holidays/calendars/islamic.py +++ b/holidays/calendars/islamic.py @@ -60,7 +60,6 @@ class _IslamicLunar: - ali_al_rida_death_dates: tuple[int, int] | tuple[tuple[int, int], ...] ali_birthday_dates: tuple[int, int] | tuple[tuple[int, int], ...] ali_death_dates: tuple[int, int] | tuple[tuple[int, int], ...] @@ -87,7 +86,7 @@ class _IslamicLunar: sadiq_birthday_dates: tuple[int, int] | tuple[tuple[int, int], ...] sadiq_death_dates: tuple[int, int] | tuple[tuple[int, int], ...] tasua_dates: tuple[int, int] | tuple[tuple[int, int], ...] - + ALI_AL_RIDA_DEATH_DATES = { 1924: (SEP, 28), 1925: (SEP, 18), diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py index 3eb214066e..a428c98908 100644 --- a/scripts/calendar/mabims_generator.py +++ b/scripts/calendar/mabims_generator.py @@ -90,13 +90,12 @@ def __init__(self) -> None: ref_h_year = 1342 ref_h_month = 1 ref_mse = (ref_h_year - 1) * 12 + (ref_h_month - 1) - + approx_days = int(ref_mse * MEAN_SYNODIC_MONTH) ref_approx = HIJRI_EPOCH + timedelta(days=approx_days) - + ref_idx = min( - range(len(self.new_moons)), - key=lambda i: abs((self.new_moons[i] - ref_approx).days) + range(len(self.new_moons)), key=lambda i: abs((self.new_moons[i] - ref_approx).days) ) self.month_offset = ref_mse - ref_idx From 65b762c42650aadbca7c4781026b3bff1e09f0d7 Mon Sep 17 00:00:00 2001 From: akshiDhi Date: Tue, 7 Jul 2026 11:23:52 +0530 Subject: [PATCH 33/33] Refactor and optimize mabims_generator visibility checks --- scripts/calendar/mabims_generator.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/scripts/calendar/mabims_generator.py b/scripts/calendar/mabims_generator.py index a428c98908..0472384d71 100644 --- a/scripts/calendar/mabims_generator.py +++ b/scripts/calendar/mabims_generator.py @@ -77,6 +77,11 @@ def __init__(self) -> None: self.observer = wgs84.latlon( SINGAPORE_LAT * N, SINGAPORE_LON * E, elevation_m=SINGAPORE_ELEV ) + self.earth = self.eph["earth"] + self.moon = self.eph["moon"] + self.sun = self.eph["sun"] + self.obs = self.earth + self.observer + self.sunset_func = almanac.sunrise_sunset(self.eph, self.observer) # Precalculate all new moons for the entire timeframe (1923-2052) chunked by year self.new_moons = [] @@ -106,11 +111,9 @@ def get_new_moon_date(self, h_year: int, h_month: int) -> date: def check_mabims_visibility(self, check_date: date) -> bool: """Check if crescent moon meets MABIMS criteria at Singapore sunset.""" - # Find exact sunset time for Singapore. - t0 = self.ts.utc(check_date.year, check_date.month, check_date.day, 9, 0) # 5pm SGT - t1 = self.ts.utc(check_date.year, check_date.month, check_date.day, 12, 0) # 8pm SGT - f = almanac.sunrise_sunset(self.eph, self.observer) - times, events = almanac.find_discrete(t0, t1, f) + t0 = self.ts.utc(check_date.year, check_date.month, check_date.day, 9, 0) + t1 = self.ts.utc(check_date.year, check_date.month, check_date.day, 12, 0) + times, events = almanac.find_discrete(t0, t1, self.sunset_func) t = None for st, e in zip(times, events): if e == 0: # sunset @@ -120,17 +123,12 @@ def check_mabims_visibility(self, check_date: date) -> bool: # Fallback to approximate Singapore sunset (~6:50pm SGT = 10:50 UTC). t = self.ts.utc(check_date.year, check_date.month, check_date.day, 10, 50, 0) - earth = self.eph["earth"] - moon = self.eph["moon"] - sun = self.eph["sun"] - - obs = earth + self.observer - moon_pos = obs.at(t).observe(moon).apparent() - sun_pos = obs.at(t).observe(sun).apparent() + moon_pos = self.obs.at(t).observe(self.moon).apparent() + sun_pos = self.obs.at(t).observe(self.sun).apparent() moon_alt, _, _ = moon_pos.altaz() - # Geocentric elongation. + # Elongation. moon_ra, moon_dec, _ = moon_pos.radec() sun_ra, sun_dec, _ = sun_pos.radec() @@ -164,7 +162,7 @@ def get_hijri_month_start(self, h_year: int, h_month: int) -> date: if self.check_mabims_visibility(check_date): return check_date + timedelta(days=1) - # Fallback: 30 days after previous month start. + # Fallback: assume crescent was visible on first checked day. return new_moon + timedelta(days=1) @@ -180,7 +178,6 @@ def generate_data() -> None: print(f"Generating MABIMS dates for Hijri years {h_start}-{h_end}...") # noqa: T201 - # Now calculate holiday dates. for name, (h_month, h_day) in MABIMS_HOLIDAYS.items(): for h_year in range(h_start, h_end + 1): if h_year % 10 == 0 and name == "HIJRI_NEW_YEAR":