Repository: pandas-dev/pandas
Pull request #61666 (2025-06-16 05:19:20)

ENH: Support for Orthodox Easter
- [X] closes #61665
- [X] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [X] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [X] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [X] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.



Patch:
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 5c53267158..6de0319873 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -64,6 +64,7 @@ Other enhancements
 - :meth:`Series.nlargest` uses a 'stable' sort internally and will preserve original ordering.
 - :class:`ArrowDtype` now supports ``pyarrow.JsonType`` (:issue:`60958`)
 - :class:`DataFrameGroupBy` and :class:`SeriesGroupBy` methods ``sum``, ``mean``, ``median``, ``prod``, ``min``, ``max``, ``std``, ``var`` and ``sem`` now accept ``skipna`` parameter (:issue:`15675`)
+- :class:`Easter` has gained a new constructor argument ``method`` which specifies the method used to calculate Easter — for example, Orthodox Easter (:issue:`61665`)
 - :class:`Holiday` has gained the constructor argument and field ``exclude_dates`` to exclude specific datetimes from a custom holiday calendar (:issue:`54382`)
 - :class:`Rolling` and :class:`Expanding` now support ``nunique`` (:issue:`26958`)
 - :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`)
diff --git a/pandas/_libs/tslibs/offsets.pyi b/pandas/_libs/tslibs/offsets.pyi
index ad579a5e41..a71aa42b4f 100644
--- a/pandas/_libs/tslibs/offsets.pyi
+++ b/pandas/_libs/tslibs/offsets.pyi
@@ -230,7 +230,13 @@ class FY5253Quarter(FY5253Mixin):
         variation: Literal["nearest", "last"] = ...,
     ) -> None: ...
 
-class Easter(SingleConstructorOffset): ...
+class Easter(SingleConstructorOffset):
+    def __init__(
+        self,
+        n: int = ...,
+        normalize: bool = ...,
+        method: int = ...,
+    ) -> None: ...
 
 class _CustomBusinessMonth(BusinessMixin):
     def __init__(
diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx
index a16964435e..87214c3758 100644
--- a/pandas/_libs/tslibs/offsets.pyx
+++ b/pandas/_libs/tslibs/offsets.pyx
@@ -4520,6 +4520,12 @@ cdef class Easter(SingleConstructorOffset):
         The number of years represented.
     normalize : bool, default False
         Normalize start/end dates to midnight before generating date range.
+    method : int, default 3
+        The method used to calculate the date of Easter. Valid options are:
+        - 1 (EASTER_JULIAN): Original calculation in Julian calendar
+        - 2 (EASTER_ORTHODOX): Original method, date converted to Gregorian calendar
+        - 3 (EASTER_WESTERN): Revised method, in Gregorian calendar
+        These constants are defined in the `dateutil.easter` module.
 
     See Also
     --------
@@ -4532,15 +4538,32 @@ cdef class Easter(SingleConstructorOffset):
     Timestamp('2022-04-17 00:00:00')
     """
 
+    _attributes = tuple(["n", "normalize", "method"])
+
+    cdef readonly:
+        int method
+
+    from dateutil.easter import EASTER_WESTERN
+
+    def __init__(self, n=1, normalize=False, method=EASTER_WESTERN):
+        BaseOffset.__init__(self, n, normalize)
+
+        self.method = method
+
+        if method < 1 or method > 3:
+            raise ValueError(f"Method must be 1<=method<=3, got {method}")
+
     cpdef __setstate__(self, state):
+        from dateutil.easter import EASTER_WESTERN
         self.n = state.pop("n")
         self.normalize = state.pop("normalize")
+        self.method = state.pop("method", EASTER_WESTERN)
 
     @apply_wraps
     def _apply(self, other: datetime) -> datetime:
         from dateutil.easter import easter
 
-        current_easter = easter(other.year)
+        current_easter = easter(other.year, method=self.method)
         current_easter = datetime(
             current_easter.year, current_easter.month, current_easter.day
         )
@@ -4555,7 +4578,7 @@ cdef class Easter(SingleConstructorOffset):
 
         # NOTE: easter returns a datetime.date so we have to convert to type of
         # other
-        new = easter(other.year + n)
+        new = easter(other.year + n, method=self.method)
         new = datetime(
             new.year,
             new.month,
@@ -4573,7 +4596,7 @@ cdef class Easter(SingleConstructorOffset):
 
         from dateutil.easter import easter
 
-        return date(dt.year, dt.month, dt.day) == easter(dt.year)
+        return date(dt.year, dt.month, dt.day) == easter(dt.year, method=self.method)
 
 
 # ----------------------------------------------------------------------


Test patch:
diff --git a/pandas/tests/tseries/offsets/test_easter.py b/pandas/tests/tseries/offsets/test_easter.py
index ada72d9443..309411ceb5 100644
--- a/pandas/tests/tseries/offsets/test_easter.py
+++ b/pandas/tests/tseries/offsets/test_easter.py
@@ -7,6 +7,10 @@ from __future__ import annotations
 
 from datetime import datetime
 
+from dateutil.easter import (
+    EASTER_ORTHODOX,
+    EASTER_WESTERN,
+)
 import pytest
 
 from pandas.tests.tseries.offsets.common import assert_offset_equal
@@ -32,3 +36,115 @@ class TestEaster:
     )
     def test_offset(self, offset, date, expected):
         assert_offset_equal(offset, date, expected)
+
+    @pytest.mark.parametrize(
+        "offset,date,expected",
+        [
+            (Easter(method=EASTER_WESTERN), datetime(2010, 1, 1), datetime(2010, 4, 4)),
+            (
+                Easter(method=EASTER_WESTERN),
+                datetime(2010, 4, 5),
+                datetime(2011, 4, 24),
+            ),
+            (
+                Easter(2, method=EASTER_WESTERN),
+                datetime(2010, 1, 1),
+                datetime(2011, 4, 24),
+            ),
+            (
+                Easter(method=EASTER_WESTERN),
+                datetime(2010, 4, 4),
+                datetime(2011, 4, 24),
+            ),
+            (
+                Easter(2, method=EASTER_WESTERN),
+                datetime(2010, 4, 4),
+                datetime(2012, 4, 8),
+            ),
+            (
+                -Easter(method=EASTER_WESTERN),
+                datetime(2011, 1, 1),
+                datetime(2010, 4, 4),
+            ),
+            (
+                -Easter(method=EASTER_WESTERN),
+                datetime(2010, 4, 5),
+                datetime(2010, 4, 4),
+            ),
+            (
+                -Easter(2, method=EASTER_WESTERN),
+                datetime(2011, 1, 1),
+                datetime(2009, 4, 12),
+            ),
+            (
+                -Easter(method=EASTER_WESTERN),
+                datetime(2010, 4, 4),
+                datetime(2009, 4, 12),
+            ),
+            (
+                -Easter(2, method=EASTER_WESTERN),
+                datetime(2010, 4, 4),
+                datetime(2008, 3, 23),
+            ),
+        ],
+    )
+    def test_western_easter_offset(self, offset, date, expected):
+        assert_offset_equal(offset, date, expected)
+
+    @pytest.mark.parametrize(
+        "offset,date,expected",
+        [
+            (
+                Easter(method=EASTER_ORTHODOX),
+                datetime(2010, 1, 1),
+                datetime(2010, 4, 4),
+            ),
+            (
+                Easter(method=EASTER_ORTHODOX),
+                datetime(2010, 4, 5),
+                datetime(2011, 4, 24),
+            ),
+            (
+                Easter(2, method=EASTER_ORTHODOX),
+                datetime(2010, 1, 1),
+                datetime(2011, 4, 24),
+            ),
+            (
+                Easter(method=EASTER_ORTHODOX),
+                datetime(2010, 4, 4),
+                datetime(2011, 4, 24),
+            ),
+            (
+                Easter(2, method=EASTER_ORTHODOX),
+                datetime(2010, 4, 4),
+                datetime(2012, 4, 15),
+            ),
+            (
+                -Easter(method=EASTER_ORTHODOX),
+                datetime(2011, 1, 1),
+                datetime(2010, 4, 4),
+            ),
+            (
+                -Easter(method=EASTER_ORTHODOX),
+                datetime(2010, 4, 5),
+                datetime(2010, 4, 4),
+            ),
+            (
+                -Easter(2, method=EASTER_ORTHODOX),
+                datetime(2011, 1, 1),
+                datetime(2009, 4, 19),
+            ),
+            (
+                -Easter(method=EASTER_ORTHODOX),
+                datetime(2010, 4, 4),
+                datetime(2009, 4, 19),
+            ),
+            (
+                -Easter(2, method=EASTER_ORTHODOX),
+                datetime(2010, 4, 4),
+                datetime(2008, 4, 27),
+            ),
+        ],
+    )
+    def test_orthodox_easter_offset(self, offset, date, expected):
+        assert_offset_equal(offset, date, expected)
diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py
index f5c2c06162..0b2e66a2b3 100644
--- a/pandas/tests/tseries/offsets/test_offsets.py
+++ b/pandas/tests/tseries/offsets/test_offsets.py
@@ -239,7 +239,7 @@ class TestCommon:
         offset = _create_offset(offset_types)
 
         freqstr = offset.freqstr
-        if freqstr not in ("<Easter>", "<DateOffset: days=1>", "LWOM-SAT"):
+        if freqstr not in ("<Easter: method=3>", "<DateOffset: days=1>", "LWOM-SAT"):
             code = _get_offset(freqstr)
             assert offset.rule_code == code