Add astronomical Sinhala calendar implementation#3672
Add astronomical Sinhala calendar implementation#3672aadesh-shrivastava wants to merge 2 commits into
Conversation
Summary by CodeRabbit
WalkthroughAdds a new ChangesSinhala Astronomy Calculations
Estimated code review effort: 4 (Complex) | ~45 minutes Related issues: Suggested labels: enhancement, calendars Suggested reviewers: Sinhala calendar module lands here, so someone familiar with Sri Lanka holiday logic and astronomical calendar math should take a look. Poem A rabbit checks the moon tonight, 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@holidays/calendars/sinhala_astronomy.py`:
- Around line 27-35: The module-level initialization in sinhala_astronomy is
eagerly downloading and loading ephemeris/catalog data during import, which
creates offline/CI failures and unexpected side effects. Move the
load.timescale(), load("de440s.bsp"), and hipparcos.load_dataframe(...) work
behind a lazy accessor used by the astronomy functions instead of executing it
at import time. Add caching in a helper such as a private
_get_eph/_get_stars-style function so the data is fetched once on first use and
reused afterward.
- Line 113: The `zip()` usages in `sinhala_astronomy.py` are missing an explicit
`strict=` argument, which Ruff flags as B905. Update the `for t, moon_phase in
zip(times, phases)` loop and the other `zip()` calls in this module (including
the ones near the `find_discrete` results) to pass `strict=True`, since the
paired sequences are expected to be the same length and this makes that
invariant explicit.
- Around line 23-25: The module-level imports in sinhala_astronomy.py depend on
skyfield, but it is not declared as a runtime dependency, so add skyfield to
project.dependencies in pyproject.toml with an explicit version pin. Update the
dependency list used by the Sinhala astronomy path so imports like
skyfield.almanac and skyfield.api in sinhala_astronomy.py resolve on a clean
install.
- Around line 179-219: The full-moon search in find_full_moons() is anchored to
a UTC year, so events near the boundary can be assigned to the wrong local
Gregorian year in generate_poya_dates(). Update the logic to determine poya
dates by local Sri Lanka time instead of raw UTC, either by filtering on
local_datetime.date().year in generate_poya_dates() or by searching the moon
phases using a local-time window so boundary moons are returned under the
correct year.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bc5592f5-8c33-4c07-bffa-ae116ddf6939
📒 Files selected for processing (1)
holidays/calendars/sinhala_astronomy.py
| from skyfield import almanac | ||
| from skyfield.api import Star, load, wgs84 | ||
| from skyfield.data import hipparcos |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm skyfield is not declared anywhere in project metadata.
fd -t f 'pyproject.toml|setup.cfg|setup.py|requirements.*txt' | while read -r f; do
echo "== $f =="
rg -n -i 'skyfield' "$f" || echo " (no skyfield reference)"
doneRepository: vacanza/holidays
Length of output: 201
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== metadata files =="
git ls-files 'pyproject.toml' 'setup.cfg' 'setup.py' 'requirements*.txt' 'holidays/calendars/sinhala_astronomy.py'
echo
echo "== pyproject.toml (relevant sections) =="
sed -n '1,260p' pyproject.toml
echo
echo "== sinhala_astronomy.py (relevant lines) =="
cat -n holidays/calendars/sinhala_astronomy.py | sed -n '1,120p'Repository: vacanza/holidays
Length of output: 9050
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import tomllib
pyproject = Path("pyproject.toml")
data = tomllib.loads(pyproject.read_text())
# Print dependency-related keys compactly
project = data.get("project", {})
print("project.dependencies:", project.get("dependencies"))
print("project.optional-dependencies:", project.get("optional-dependencies"))
print("tool.poetry.dependencies:", data.get("tool", {}).get("poetry", {}).get("dependencies"))
print("dependency-groups:", data.get("dependency-groups"))
PYRepository: vacanza/holidays
Length of output: 911
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import ast
path = Path("holidays/calendars/sinhala_astronomy.py")
tree = ast.parse(path.read_text(), filename=str(path))
imports = []
for node in tree.body:
if isinstance(node, ast.ImportFrom):
imports.append((node.level, node.module, [alias.name for alias in node.names]))
elif isinstance(node, ast.Import):
imports.append((0, None, [alias.name for alias in node.names]))
print(imports)
PYRepository: vacanza/holidays
Length of output: 314
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import tomllib
data = tomllib.loads(Path("pyproject.toml").read_text())
for key in ["project", "tool"]:
if key in data:
print(key, "present")
# show any mention of skyfield across repo metadata if present
for p in [Path("pyproject.toml"), Path("setup.cfg"), Path("setup.py")]:
if p.exists():
text = p.read_text()
if "skyfield" in text.lower():
print(f"{p}: contains skyfield")
PYRepository: vacanza/holidays
Length of output: 183
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== skyfield references =="
rg -n -i '\bskyfield\b' .
echo
echo "== calendar package imports =="
cat -n holidays/calendars/__init__.py | sed -n '1,220p'
echo
echo "== any Sinhala calendar entry points =="
rg -n -i 'sinhala_astronomy|Sinhala' holidaysRepository: vacanza/holidays
Length of output: 7462
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import ast
for path_str in ["holidays/calendars/__init__.py", "holidays/__init__.py"]:
path = Path(path_str)
print(f"== {path_str} ==")
tree = ast.parse(path.read_text(), filename=path_str)
for node in tree.body:
if isinstance(node, ast.ImportFrom):
print("from", "." * node.level + (node.module or ""), "import", [a.name for a in node.names])
elif isinstance(node, ast.Import):
print("import", [a.name for a in node.names])
PYRepository: vacanza/holidays
Length of output: 1645
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import ast
path = Path("holidays/calendars/sinhala_astronomy.py")
tree = ast.parse(path.read_text(), filename=str(path))
# Detect whether skyfield imports are guarded
for node in tree.body:
if isinstance(node, ast.Try):
print("found try block guarding imports")
break
else:
print("no try block guarding imports")
PYRepository: vacanza/holidays
Length of output: 184
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for path_str in ["holidays/calendars/__init__.py", "holidays/__init__.py"]:
path = Path(path_str)
print(f"== {path_str} ==")
print(path.read_text())
PYRepository: vacanza/holidays
Length of output: 2876
Declare skyfield as a runtime dependency
holidays/calendars/sinhala_astronomy.py imports skyfield at module load time, but pyproject.toml only lists python-dateutil. Add skyfield to project.dependencies with a version pin so the Sinhala astronomy path doesn’t fail on a clean install.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@holidays/calendars/sinhala_astronomy.py` around lines 23 - 25, The
module-level imports in sinhala_astronomy.py depend on skyfield, but it is not
declared as a runtime dependency, so add skyfield to project.dependencies in
pyproject.toml with an explicit version pin. Update the dependency list used by
the Sinhala astronomy path so imports like skyfield.almanac and skyfield.api in
sinhala_astronomy.py resolve on a clean install.
| ts = load.timescale() | ||
| eph = load("de440s.bsp") | ||
|
|
||
| earth = eph["earth"] | ||
| sun = eph["sun"] | ||
| moon = eph["moon"] | ||
|
|
||
| with load.open(hipparcos.URL) as f: | ||
| stars = hipparcos.load_dataframe(f) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Import-time downloads and file writes will bite users in offline/CI environments.
Loading the ephemeris and Hipparcos catalog at module import triggers network downloads (~32 MB for de440s.bsp plus the Hipparcos data) and writes those files into the current working directory. For a widely-imported library this is a heavy, surprising side effect: importing the module fails without network access, pollutes the cwd, and slows startup. Consider deferring all of this behind a lazy loader/cache that runs on first use rather than at import.
♻️ Sketch: lazy initialization
_eph = None
def _get_eph():
global _eph
if _eph is None:
_eph = load("de440s.bsp")
return _eph🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@holidays/calendars/sinhala_astronomy.py` around lines 27 - 35, The
module-level initialization in sinhala_astronomy is eagerly downloading and
loading ephemeris/catalog data during import, which creates offline/CI failures
and unexpected side effects. Move the load.timescale(), load("de440s.bsp"), and
hipparcos.load_dataframe(...) work behind a lazy accessor used by the astronomy
functions instead of executing it at import time. Add caching in a helper such
as a private _get_eph/_get_stars-style function so the data is fetched once on
first use and reused afterward.
| prev_new_moon = None | ||
| next_new_moon = None | ||
|
|
||
| for t, moon_phase in zip(times, phases): |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add explicit strict= to zip() calls (Ruff B905).
Flagged here and also at lines 139 and 192. times and phases from find_discrete share a length, so strict=True documents that invariant and future-proofs against silent truncation.
♻️ Proposed fix
- for t, moon_phase in zip(times, phases):
+ for t, moon_phase in zip(times, phases, strict=True):📝 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.
| for t, moon_phase in zip(times, phases): | |
| for t, moon_phase in zip(times, phases, strict=True): |
🧰 Tools
🪛 Ruff (0.15.20)
[warning] 113-113: zip() without an explicit strict= parameter
Add explicit value for parameter strict=
(B905)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@holidays/calendars/sinhala_astronomy.py` at line 113, The `zip()` usages in
`sinhala_astronomy.py` are missing an explicit `strict=` argument, which Ruff
flags as B905. Update the `for t, moon_phase in zip(times, phases)` loop and the
other `zip()` calls in this module (including the ones near the `find_discrete`
results) to pass `strict=True`, since the paired sequences are expected to be
the same length and this makes that invariant explicit.
Source: Linters/SAST tools
| def find_full_moons(year): | ||
|
|
||
| start = ts.utc(year, 1, 1) | ||
| end = ts.utc(year + 1, 1, 1) | ||
|
|
||
| times, phases = almanac.find_discrete( | ||
| start, | ||
| end, | ||
| almanac.moon_phases(eph), | ||
| ) | ||
|
|
||
| full_moons = [] | ||
|
|
||
| for t, phase in zip(times, phases): | ||
| if phase == 2: | ||
| full_moons.append(t) | ||
|
|
||
| return full_moons | ||
|
|
||
|
|
||
| def generate_poya_dates(year): | ||
| """ | ||
| Generate astronomical Poya observations for a Gregorian year. | ||
|
|
||
| The returned civil date is the Sri Lanka local date containing the | ||
| astronomical full moon. Sinhala month assignment is determined using | ||
| ``get_fullmoon_month()``. | ||
| """ | ||
|
|
||
| poya_dates = [] | ||
|
|
||
| for full_moon in find_full_moons(year): | ||
| local_datetime = full_moon.utc_datetime().astimezone(SL_TIMEZONE) | ||
|
|
||
| poya_dates.append( | ||
| { | ||
| "date": local_datetime.date(), | ||
| "month": get_fullmoon_month(local_datetime.date()), | ||
| "full_moon": local_datetime, | ||
| } | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the file and locate the relevant symbols.
git ls-files holidays/calendars/sinhala_astronomy.py
ast-grep outline holidays/calendars/sinhala_astronomy.py --view expanded
# Show the relevant lines with numbering.
sed -n '1,280p' holidays/calendars/sinhala_astronomy.py | cat -nRepository: vacanza/holidays
Length of output: 8182
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Run a small read-only probe to see whether a full moon near a UTC year boundary
# can shift into a different local civil date/year in Asia/Colombo.
python3 - <<'PY'
from datetime import timezone
from zoneinfo import ZoneInfo
try:
from skyfield.api import load
from skyfield import almanac
except Exception as e:
print("SKYFIELD_IMPORT_ERROR:", type(e).__name__, e)
raise SystemExit(0)
ts = load.timescale()
eph = load('de421.bsp')
SL = ZoneInfo("Asia/Colombo")
for year in range(2020, 2031):
start = ts.utc(year, 1, 1)
end = ts.utc(year + 1, 1, 1)
times, phases = almanac.find_discrete(start, end, almanac.moon_phases(eph))
for t, phase in zip(times, phases):
if phase == 2:
utc_dt = t.utc_datetime().replace(tzinfo=timezone.utc)
local_dt = utc_dt.astimezone(SL)
if utc_dt.year != local_dt.year:
print(
"YEAR_MISMATCH",
year,
"utc=", utc_dt.isoformat(),
"local=", local_dt.isoformat(),
)
PYRepository: vacanza/holidays
Length of output: 224
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Probe the exact function behavior in the repo without executing repo code:
# emulate the logic of find_full_moons()/generate_poya_dates() against Skyfield.
python3 - <<'PY'
from datetime import timezone
from zoneinfo import ZoneInfo
try:
from skyfield.api import load
from skyfield import almanac
except Exception as e:
print("SKYFIELD_IMPORT_ERROR:", type(e).__name__, e)
raise SystemExit(0)
ts = load.timescale()
eph = load('de421.bsp')
SL = ZoneInfo("Asia/Colombo")
hits = []
for year in range(1900, 2051):
start = ts.utc(year, 1, 1)
end = ts.utc(year + 1, 1, 1)
times, phases = almanac.find_discrete(start, end, almanac.moon_phases(eph))
for t, phase in zip(times, phases):
if phase == 2:
utc_dt = t.utc_datetime().replace(tzinfo=timezone.utc)
local_dt = utc_dt.astimezone(SL)
if utc_dt.year != local_dt.year:
hits.append((year, utc_dt.isoformat(), local_dt.isoformat()))
print("HITS", len(hits))
for h in hits[:20]:
print("YEAR_MISMATCH", *h)
PYRepository: vacanza/holidays
Length of output: 224
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check how generate_poya_dates() is consumed and whether the year is
# interpreted from the local civil date or the UTC search window.
rg -n "generate_poya_dates\(|find_full_moons\(" -S .Repository: vacanza/holidays
Length of output: 394
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for tests or documentation around poya date boundaries.
rg -n "poya|full moon|Sinhala" -S tests holidays docs .Repository: vacanza/holidays
Length of output: 33835
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect nearby code for any year-based assumptions around the returned dates.
sed -n '1,260p' holidays/calendars/sinhala_astronomy.py | cat -nRepository: vacanza/holidays
Length of output: 7310
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
# Pure date-boundary sanity probe: if a UTC instant near year-end is converted
# to Asia/Colombo, it can land in the next local year and still be excluded
# from the next UTC year window.
utc = timezone.utc
sl = ZoneInfo("Asia/Colombo")
for dt in [
datetime(2024, 12, 31, 18, 31, tzinfo=utc), # just after local midnight
datetime(2024, 12, 31, 20, 0, tzinfo=utc),
]:
local = dt.astimezone(sl)
print(dt.isoformat(), "->", local.isoformat(), "UTC year", dt.year, "local year", local.year)
PYRepository: vacanza/holidays
Length of output: 2378
🌐 Web query:
full moon December 31 UTC Sri Lanka 5:30 year boundary
💡 Result:
There is no full moon on December 31, 2026. The final full moon of December 2026 occurred on December 24, 2026, at 01:28 UTC [1]. By December 31, 2026, the moon had progressed to a waning crescent phase [2][3], following a last quarter phase on December 30, 2026 [4][1][5]. This holds true regardless of the time zone, including in Sri Lanka (UTC+5:30) [6][7][8].
Citations:
- 1: https://www.moongiant.com/moonphases/december/2026/
- 2: https://theskylive.com/moon/2026-12-31
- 3: https://moonphasetonight.com/date/2026-12-31
- 4: https://lunaf.com/lunar-calendar/2026/12/31/
- 5: https://theskylive.com/moon-calendar?month=12&year=2026
- 6: https://www.timeanddate.com/moon/sri-lanka/sri-jayawardenapura-kotte?month=12&year=2026
- 7: https://www.timeanddate.com/moon/@1227604?month=12
- 8: https://www.mooninfo.org/world/sri-lanka/159030/moon-phases-2026-for-colombo.html
🌐 Web query:
full moon December 31 UTC site:timeanddate.com moon Sri Lanka January 1
💡 Result:
There is no Full Moon on December 31, 2026, or January 1, 2027. Based on the lunar cycle, the December 2026 Full Moon occurs on December 23, 2026 [1]. The subsequent Full Moon, known as the Wolf Moon, occurs on January 22, 2027 [2][3]. Therefore, neither date coincides with a Full Moon in UTC or Sri Lanka time.
Citations:
- 1: https://www.timeanddate.com/astronomy/moon/cold.html
- 2: https://www.timeanddate.com/astronomy/moon/wolf.html
- 3: http://timeanddate.com/astronomy/moon/wolf.html
🌐 Web query:
full moon December 31 UTC site:timeanddate.com/moon/ 2024 2025 2027 2028 January 1 Sri Lanka
💡 Result:
Based on lunar cycle data, here is the information regarding Full Moons on or around December 31 for the requested years: - December 31, 2024: There is no Full Moon on this date. The nearest full moon occurred on December 15, 2024 [1][2]. - December 31, 2025: There is no Full Moon on this date. The last full moon of 2025 occurred on December 4, 2025 [3][4]. - December 31, 2027: There is no Full Moon on this date. The nearest full moon occurred on December 13, 2027 [5]. - December 31, 2028: There is a Full Moon on this date. It occurs at 08:48 UTC [6]. Because this is the second full moon to occur in the same calendar month, it is also known as a Blue Moon [6]. Regarding January 1 in Sri Lanka: - January 1, 2025: There is no Full Moon on this date. The full moon occurred on January 14, 2025 (as seen in data for nearby locations like Bengaluru and Sri Lanka) [7][8].
Citations:
- 1: https://www.timeanddate.com/moon/phases/?year=&n=702
- 2: https://www.timeanddate.com/moon/phases/@4155749
- 3: https://www.timeanddate.com/moon/phases/?year=2025
- 4: https://www.timeanddate.com/moon/phases/@31.54577,-97.13013
- 5: https://www.timeanddate.com/moon/phases/?year=2027
- 6: https://www.timeanddate.com/moon/phases/?year=2028&n=0
- 7: https://www.timeanddate.com/moon/phases/@1232781
- 8: https://www.timeanddate.com/moon/india/bengaluru?month=1
🌐 Web query:
full moon December 31 UTC +5:30 January 1 local year boundary example
💡 Result:
The astronomical full moon is a global event that occurs at a single, precise moment in time, regardless of an observer's location [1][2][3]. Because this event is defined by the physical alignment of the Earth, Moon, and Sun [2][4], the date and time of the full moon are converted into local time zones by adding or subtracting the relevant time offset from Coordinated Universal Time (UTC) [1][3]. An example of a full moon crossing a year boundary between December 31 and January 1 occurs when the precise UTC time of the full moon is close enough to midnight that time zone offsets push the date into the following year for some locations. A notable past example occurred on December 31, 2028 (at 16:48 UTC), which resulted in the full moon falling on January 1, 2029, in time zones east of UTC [5]. Regarding your specific interest in UTC+5:30 (the time zone for India): - A full moon occurring late on December 31 in UTC (e.g., between 18:30 and 23:59 UTC) would shift to January 1 in UTC+5:30 because the local time would be 5 hours and 30 minutes ahead of UTC. - In the case of the 2028 event (16:48 UTC), it was already January 1, 2029, in India (at 22:18 IST) [5]. Conversely, if a full moon occurs very early on January 1 in UTC (e.g., between 00:00 and 05:30 UTC), it would still be December 31 in regions to the west of the prime meridian, while being January 1 in UTC+5:30. Because of this, the calendar date of the full moon can differ across the globe, even though the astronomical event is simultaneous for everyone [1][2].
Citations:
- 1: https://www.timeanddate.com/moon/phases/help
- 2: https://astronomy.stackexchange.com/questions/20947/is-the-full-moon-full-everywhere-on-the-planet
- 3: https://www.fullmoon.info/en/fullmoon-knowledge/fullmoon-worldwide.html
- 4: https://www.timeanddate.com/astronomy/moon/full-moon-daytime.html
- 5: https://lunaf.com/lunar-calendar/2029/01/01/
🌐 Web query:
site:timeanddate.com/moon/phases/?year=2028 "December 31, 2028" full moon UTC
💡 Result:
The full moon on December 31, 2028, will occur at 08:48 UTC [1]. It is also noted as a "Blue Moon," as it is the second full moon to occur within the same calendar month [1].
Citations:
Full moons near UTC year boundaries can land in the next local year. find_full_moons() scans a UTC year window, so a moon like Dec 31, 2028 08:48 UTC becomes Jan 1, 2029 in Sri Lanka and is emitted under 2028, not 2029. Filter on local_datetime.date().year or search in local time.
🧰 Tools
🪛 Ruff (0.15.20)
[warning] 192-192: zip() without an explicit strict= parameter
Add explicit value for parameter strict=
(B905)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@holidays/calendars/sinhala_astronomy.py` around lines 179 - 219, The
full-moon search in find_full_moons() is anchored to a UTC year, so events near
the boundary can be assigned to the wrong local Gregorian year in
generate_poya_dates(). Update the logic to determine poya dates by local Sri
Lanka time instead of raw UTC, either by filtering on local_datetime.date().year
in generate_poya_dates() or by searching the moon phases using a local-time
window so boundary moons are returned under the correct year.
There was a problem hiding this comment.
3 issues found and verified against the latest diff
Confidence score: 2/5
- In
holidays/calendars/sinhala_astronomy.py,skyfieldis imported unconditionally without being declared as a dependency, so clean installs can fail immediately withModuleNotFoundErrorand break module usage on import — addskyfieldas an explicit, pinned dependency before merging. holidays/calendars/sinhala_astronomy.pycurrently loads ephemeris/catalog/timescale data at import time, and Skyfield may download files duringload(), which can make imports slow, flaky, or fail in offline/CI environments — move dataset loading behind lazy initialization (or explicit setup) so import stays deterministic.- The year-boundary handling around full-moon conversion in
holidays/calendars/sinhala_astronomy.py(e.g., UTC Dec 31 becoming local Jan 1) can drop valid local dates from the requested year and produce incorrect holiday results near New Year — adjustfind_full_moon_daysyear filtering to use local-date boundaries and add a regression test for the Dec/Jan edge case before merging.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="holidays/calendars/sinhala_astronomy.py">
<violation number="1" location="holidays/calendars/sinhala_astronomy.py:25">
P1: `skyfield` is imported unconditionally here but is not declared as a project dependency. A clean install of this package will fail with `ModuleNotFoundError` when this module is loaded. `skyfield` (with a version pin) needs to be added to `project.dependencies` in `pyproject.toml`.</violation>
<violation number="2" location="holidays/calendars/sinhala_astronomy.py:28">
P1: The module eagerly loads astronomical datasets (`de440s.bsp` ephemeris, Hipparcos star catalog, timescale) at import time. Skyfield's `load()` downloads missing files from the internet, so importing this module can fail or hang in offline or restricted environments and adds unexpected startup latency. Consider moving the loading behind a lazily-initialized helper (e.g., a cached loader function or a lightweight singleton) so the network/disk IO only happens when the astronomical calculations are actually used.</violation>
<violation number="3" location="holidays/calendars/sinhala_astronomy.py:210">
P2: A full moon occurring near the end of a UTC year (e.g., Dec 31 08:48 UTC) converts to the next calendar day in Sri Lanka (UTC+5:30), producing a local date of Jan 1 of the following year. However, since `find_full_moons(year)` searches within UTC year boundaries, this event is emitted under `year` rather than `year + 1`, and conversely, a Jan 1 UTC full moon that's still Dec 31 locally won't appear. Consider filtering on `local_datetime.date().year == year` or adjusting the search window to account for the timezone offset.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
|
||
| from skyfield import almanac | ||
| from skyfield.api import Star, load, wgs84 | ||
| from skyfield.data import hipparcos |
There was a problem hiding this comment.
P1: skyfield is imported unconditionally here but is not declared as a project dependency. A clean install of this package will fail with ModuleNotFoundError when this module is loaded. skyfield (with a version pin) needs to be added to project.dependencies in pyproject.toml.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At holidays/calendars/sinhala_astronomy.py, line 25:
<comment>`skyfield` is imported unconditionally here but is not declared as a project dependency. A clean install of this package will fail with `ModuleNotFoundError` when this module is loaded. `skyfield` (with a version pin) needs to be added to `project.dependencies` in `pyproject.toml`.</comment>
<file context>
@@ -0,0 +1,221 @@
+
+from skyfield import almanac
+from skyfield.api import Star, load, wgs84
+from skyfield.data import hipparcos
+
+ts = load.timescale()
</file context>
| from skyfield.data import hipparcos | ||
|
|
||
| ts = load.timescale() | ||
| eph = load("de440s.bsp") |
There was a problem hiding this comment.
P1: The module eagerly loads astronomical datasets (de440s.bsp ephemeris, Hipparcos star catalog, timescale) at import time. Skyfield's load() downloads missing files from the internet, so importing this module can fail or hang in offline or restricted environments and adds unexpected startup latency. Consider moving the loading behind a lazily-initialized helper (e.g., a cached loader function or a lightweight singleton) so the network/disk IO only happens when the astronomical calculations are actually used.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At holidays/calendars/sinhala_astronomy.py, line 28:
<comment>The module eagerly loads astronomical datasets (`de440s.bsp` ephemeris, Hipparcos star catalog, timescale) at import time. Skyfield's `load()` downloads missing files from the internet, so importing this module can fail or hang in offline or restricted environments and adds unexpected startup latency. Consider moving the loading behind a lazily-initialized helper (e.g., a cached loader function or a lightweight singleton) so the network/disk IO only happens when the astronomical calculations are actually used.</comment>
<file context>
@@ -0,0 +1,221 @@
+from skyfield.data import hipparcos
+
+ts = load.timescale()
+eph = load("de440s.bsp")
+
+earth = eph["earth"]
</file context>
|
|
||
| poya_dates = [] | ||
|
|
||
| for full_moon in find_full_moons(year): |
There was a problem hiding this comment.
P2: A full moon occurring near the end of a UTC year (e.g., Dec 31 08:48 UTC) converts to the next calendar day in Sri Lanka (UTC+5:30), producing a local date of Jan 1 of the following year. However, since find_full_moons(year) searches within UTC year boundaries, this event is emitted under year rather than year + 1, and conversely, a Jan 1 UTC full moon that's still Dec 31 locally won't appear. Consider filtering on local_datetime.date().year == year or adjusting the search window to account for the timezone offset.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At holidays/calendars/sinhala_astronomy.py, line 210:
<comment>A full moon occurring near the end of a UTC year (e.g., Dec 31 08:48 UTC) converts to the next calendar day in Sri Lanka (UTC+5:30), producing a local date of Jan 1 of the following year. However, since `find_full_moons(year)` searches within UTC year boundaries, this event is emitted under `year` rather than `year + 1`, and conversely, a Jan 1 UTC full moon that's still Dec 31 locally won't appear. Consider filtering on `local_datetime.date().year == year` or adjusting the search window to account for the timezone offset.</comment>
<file context>
@@ -0,0 +1,221 @@
+
+ poya_dates = []
+
+ for full_moon in find_full_moons(year):
+ local_datetime = full_moon.utc_datetime().astimezone(SL_TIMEZONE)
+
</file context>
|
|
|
||
| def lahiri_ayanamsha(ts_time): | ||
|
|
||
| _, longitude, _ = earth.at(ts_time).observe(SPICA).apparent().ecliptic_latlon("date") |



Proposed change
Closes #3447
This PR introduces an astronomical backend for the Sinhala lunisolar calendar.
The implementation computes:
The month assignment algorithm was validated against the official Sri Lankan Poya calendar for the years 2005–2025 and matched 250 out of 251 historical month assignments.
This module is currently provided as a standalone astronomical implementation and is not yet integrated into the existing Sinhala holiday calculation.
While researching the official Sri Lankan Panchanga, I was unable to find publicly available documentation describing the exact civil-date selection rule used after the astronomical full moon is determined. Rather than introducing an undocumented assumption into the existing implementation, this PR focuses on contributing the astronomical engine first.
Type of change
holidaysfunctionality in general)Checklist
make checklocally; all checks and tests passed.