Skip to content

Add grammar & converter for Christian holidays#162

Open
rlskoeser wants to merge 1 commit intodevelopfrom
feature/christian-holidays
Open

Add grammar & converter for Christian holidays#162
rlskoeser wants to merge 1 commit intodevelopfrom
feature/christian-holidays

Conversation

@rlskoeser
Copy link
Copy Markdown
Member

@rlskoeser rlskoeser commented Apr 10, 2026

Adds a new grammar and converter for Christian holidays; both standalone and omnibus.

Adapted from code originally written from Hale/Eliot dataset project; converted & updated with help from OpenCode and Claude

Summary by CodeRabbit

  • New Features

    • Added support for parsing Christian liturgical and holiday dates, including movable observances (Easter, Pentecost) and fixed holidays (Epiphany, St. Patrick's Day), with optional year specification
    • Introduced a Weekday enumeration for weekday representations
  • Tests

    • Expanded test coverage for Christian liturgical date parsing

- Adapted from Hale/Eliot dataset code
- Converted with help from OpenCode and Claude
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

Walkthrough

This PR extends the combined date parser to support Christian liturgical holidays and movable feasts. It introduces a new HolidayDateConverter that parses holiday expressions (e.g., "Easter 1916", "Epiphany 1921"), integrates the holiday grammar into the combined parser, adds a Weekday enum to the date module, and includes comprehensive test coverage for the new functionality.

Changes

Cohort / File(s) Summary
Combined Parser Integration
src/undate/converters/combined.py, src/undate/converters/grammars/combined.lark
Updated documentation to include Christian liturgical dates as fully supported (not experimental). Added HolidayTransformer to combined_transformer under the holidays key. Extended the combined grammar start rule to include holidays__holiday_date alternative and added relative import for .holidays.holiday_date.
Holiday Date Grammar
src/undate/converters/grammars/holidays.lark
New grammar file defining holiday date parsing with rules for movable feasts (Easter, Pentecost, etc.) and fixed-date holidays (Epiphany, Candlemass, St. Patrick's, etc.). Both types support optional year tokens (\d{4}).
Holiday Date Converter
src/undate/converters/holidays.py
New converter module implementing HolidayDateConverter that parses holiday expressions into Undate objects. Handles both fixed-date holidays (via lookup table) and movable feasts (via Easter calculation using convertdate.holidays.easter()). Disables serialization via to_string.
Date Utilities
src/undate/date.py
Added Weekday IntEnum with values MONDAY=0 through SUNDAY=6. Converted Date.weekday result to explicit Python int type.
Test Coverage
tests/test_converters/test_combined_parser.py, tests/test_converters/test_holidays.py
Added five Christian liturgical test cases to combined parser tests. Created new test_holidays.py module with parametrized tests for fixed holidays, movable feasts, year inference, label preservation, weekday validation, integration with Undate.parse(), error handling, and serialization behavior.

Sequence Diagram

sequenceDiagram
    participant User
    participant HolidayDateConverter
    participant LarkParser
    participant HolidayTransformer
    participant EasterCalc as convertdate.holidays
    participant Undate

    User->>HolidayDateConverter: parse("Easter 1916")
    HolidayDateConverter->>LarkParser: Parse using holidays.lark grammar
    LarkParser->>HolidayTransformer: Movable feast + year tokens
    HolidayTransformer->>EasterCalc: easter(1916) for movable feast
    EasterCalc-->>HolidayTransformer: Easter date (month, day)
    HolidayTransformer->>HolidayTransformer: Apply feast offset<br/>(Easter+0 days)
    HolidayTransformer->>Undate: Create with month, day, year
    Undate-->>HolidayDateConverter: Undate object
    HolidayDateConverter-->>User: Undate(month=4, day=3, year=1916)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • robcast
  • jdamerow

Poem

🐰 With hops of joy through Easter dates,
Our fuzzy parser celebrates!
From fixed-day feasts to movable cheer,
Liturgical holidays throughout the year! 🥕📅

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: adding grammar and a converter for Christian holidays. It clearly reflects the primary changes across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/christian-holidays

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 98.21429% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.96%. Comparing base (78cbbbb) to head (5fd7505).
⚠️ Report is 6 commits behind head on develop.

Files with missing lines Patch % Lines
src/undate/converters/holidays.py 97.01% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #162      +/-   ##
===========================================
- Coverage    99.00%   98.96%   -0.04%     
===========================================
  Files           45       47       +2     
  Lines         2216     2327     +111     
===========================================
+ Hits          2194     2303     +109     
- Misses          22       24       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/undate/converters/holidays.py (2)

49-50: Pass the holiday calendar explicitly.

HolidayTransformer declares calendar = Calendar.GREGORIAN, but holiday_date() currently relies on Undate's default by calling Undate(**parts). Passing calendar=self.calendar here makes the converter self-contained and avoids silent behavior changes if that default ever moves.

♻️ Suggested tweak
     def holiday_date(self, items):
         parts = self._get_date_parts(items)
-        return Undate(**parts)
+        return Undate(calendar=self.calendar, **parts)

Also applies to: 75-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/undate/converters/holidays.py` around lines 49 - 50, HolidayTransformer
sets calendar = Calendar.GREGORIAN but holiday_date() (and the similar block
around the other occurrence) constructs Undate using Undate(**parts) which
relies on Undate's default; update those constructors to pass the transformer's
calendar explicitly (e.g., Undate(calendar=self.calendar, **parts)) so the
converter is self-contained and won't change behavior if Undate's default
calendar changes; locate this in the HolidayTransformer class and in the other
conversion site referenced and add calendar=self.calendar to the Undate(...)
calls.

10-12: Consider explicitly adding type stubs for convertdate to improve type-checking transparency.

While the CI workflow uses mypy --install-types, which should automatically fetch types-convertdate from typeshed, adding it to the check optional-dependencies group would make this requirement explicit and improve IDE/local development experience. You can either add "types-convertdate" to the check list in pyproject.toml, or rely on the auto-installation during CI—both approaches work, but explicit declaration is clearer for maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/undate/converters/holidays.py` around lines 10 - 12, Add an explicit
dependency on the convertdate type stubs so type-checking is deterministic:
update your project's optional-dependencies "check" group in pyproject.toml to
include "types-convertdate" (this relates to the import of convertdate/holidays
in src/undate/converters/holidays.py), or document that CI relies on mypy's
--install-types behavior; ensure the change is reflected in dependency metadata
so IDEs and local mypy runs pick up the types for convertdate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/undate/converters/grammars/holidays.lark`:
- Line 17: The grammar rule year: /\d{4}/ currently accepts "0000", which later
causes datetime.date(*holidays.easter(parts["year"])) in the movable-feast
resolver to raise ValueError; update the year rule to disallow "0000" (e.g. use
a regex that rejects all-zero year such as a negative lookahead or require the
first digit 1-9) so that inputs like "0000" fail during parsing rather than
during transformation.

---

Nitpick comments:
In `@src/undate/converters/holidays.py`:
- Around line 49-50: HolidayTransformer sets calendar = Calendar.GREGORIAN but
holiday_date() (and the similar block around the other occurrence) constructs
Undate using Undate(**parts) which relies on Undate's default; update those
constructors to pass the transformer's calendar explicitly (e.g.,
Undate(calendar=self.calendar, **parts)) so the converter is self-contained and
won't change behavior if Undate's default calendar changes; locate this in the
HolidayTransformer class and in the other conversion site referenced and add
calendar=self.calendar to the Undate(...) calls.
- Around line 10-12: Add an explicit dependency on the convertdate type stubs so
type-checking is deterministic: update your project's optional-dependencies
"check" group in pyproject.toml to include "types-convertdate" (this relates to
the import of convertdate/holidays in src/undate/converters/holidays.py), or
document that CI relies on mypy's --install-types behavior; ensure the change is
reflected in dependency metadata so IDEs and local mypy runs pick up the types
for convertdate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f81e7b2a-9863-49d0-a914-86a214cb91e9

📥 Commits

Reviewing files that changed from the base of the PR and between 60328de and 5fd7505.

📒 Files selected for processing (7)
  • src/undate/converters/combined.py
  • src/undate/converters/grammars/combined.lark
  • src/undate/converters/grammars/holidays.lark
  • src/undate/converters/holidays.py
  • src/undate/date.py
  • tests/test_converters/test_combined_parser.py
  • tests/test_converters/test_holidays.py

// holidays that are always on the same date
fixed_date: EPIPHANY | CANDLEMASS | ST_PATRICKS | ALL_FOOLS | ST_CYPRIANS

year: /\d{4}/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import datetime

for year in (0, 1):
    try:
        print(year, datetime.date(year, 1, 1).isoformat())
    except Exception as exc:
        print(year, type(exc).__name__, exc)
PY

Repository: dh-tech/undate-python

Length of output: 113


🏁 Script executed:

# Find and examine the holidays.py file
fd -t f holidays.py

Repository: dh-tech/undate-python

Length of output: 137


🏁 Script executed:

# Look at the resolver code around the mentioned lines
find . -name "holidays.py" -type f | head -5 | xargs wc -l

Repository: dh-tech/undate-python

Length of output: 104


🏁 Script executed:

# Search for the resolver code that uses datetime.date
rg -A 5 -B 5 "datetime.date\|holidays.easter" --type py

Repository: dh-tech/undate-python

Length of output: 47


🏁 Script executed:

cat -n src/undate/converters/holidays.py | head -150

Repository: dh-tech/undate-python

Length of output: 6947


🏁 Script executed:

python - <<'PY'
from convertdate import holidays
import datetime

# Test what happens when calling holidays.easter(0)
for year in (0, 1, 9999, 10000):
    try:
        easter_result = holidays.easter(year)
        print(f"holidays.easter({year}) = {easter_result}")
        # Now try to create datetime.date with it
        try:
            d = datetime.date(*easter_result)
            print(f"  datetime.date(*{easter_result}) = {d.isoformat()}")
        except Exception as e:
            print(f"  datetime.date(*{easter_result}) raises {type(e).__name__}: {e}")
    except Exception as e:
        print(f"holidays.easter({year}) raises {type(e).__name__}: {e}")
PY

Repository: dh-tech/undate-python

Length of output: 188


🏁 Script executed:

# Find project configuration files
fd -t f -e "toml" -e "txt" -e "cfg" | grep -i "setup\|requirements\|pyproject"

Repository: dh-tech/undate-python

Length of output: 79


🏁 Script executed:

# Test datetime.date year boundaries
python - <<'PY'
import datetime

# Test valid year boundaries
test_years = [1, 9999, 10000, -1]
for year in test_years:
    try:
        d = datetime.date(year, 1, 1)
        print(f"datetime.date({year}, 1, 1) = {d.isoformat()}")
    except Exception as e:
        print(f"datetime.date({year}, 1, 1) raises {type(e).__name__}: {e}")
PY

Repository: dh-tech/undate-python

Length of output: 279


🏁 Script executed:

cat -n src/undate/converters/grammars/holidays.lark

Repository: dh-tech/undate-python

Length of output: 1407


Reject 0000 before it reaches the movable-feast calculation.

The grammar currently allows year: /\d{4}/ to match 0000, but the movable-feast resolver at line 127 uses datetime.date(*holidays.easter(parts["year"])), which only supports years 1–9999. Parsing inputs like Easter 0000 will therefore result in a ValueError at transformation time instead of a normal parse failure.

Grammar fix
-year: /\d{4}/
+year: /(?!0000)\d{4}/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
year: /\d{4}/
year: /(?!0000)\d{4}/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/undate/converters/grammars/holidays.lark` at line 17, The grammar rule
year: /\d{4}/ currently accepts "0000", which later causes
datetime.date(*holidays.easter(parts["year"])) in the movable-feast resolver to
raise ValueError; update the year rule to disallow "0000" (e.g. use a regex that
rejects all-zero year such as a negative lookahead or require the first digit
1-9) so that inputs like "0000" fail during parsing rather than during
transformation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant