Skip to content

Add astronomical Sinhala calendar implementation#3672

Draft
aadesh-shrivastava wants to merge 2 commits into
vacanza:devfrom
aadesh-shrivastava:feature/sinhala-implementation
Draft

Add astronomical Sinhala calendar implementation#3672
aadesh-shrivastava wants to merge 2 commits into
vacanza:devfrom
aadesh-shrivastava:feature/sinhala-implementation

Conversation

@aadesh-shrivastava

Copy link
Copy Markdown
Contributor

Proposed change

Closes #3447

This PR introduces an astronomical backend for the Sinhala lunisolar calendar.

The implementation computes:

  • astronomical new moons and full moons
  • sidereal solar longitude using the Lahiri ayanamsha
  • Sinhala lunar month assignment
  • Adhi Māsa (intercalary month) detection
  • astronomical Poya date generation

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

  • New country/market holidays support (thank you!)
  • Supported country/market holidays update (calendar discrepancy fix, localization)
  • Existing code/documentation/test/process quality improvement (best practice, cleanup, refactoring, optimization)
  • Dependency update (version deprecation/pin/upgrade)
  • Bugfix (non-breaking change which fixes an issue)
  • Breaking change (a code change causing existing functionality to break)
  • New feature (new holidays functionality in general)

Checklist

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features
    • Added Sinhala astronomy-based calendar support for calculating lunar months and Poya dates.
    • Improved date generation with local-time conversion and month assignment for full-moon observances.
    • Added support for identifying leap months and nearby moon phases for more accurate calendar results.

Walkthrough

Adds a new holidays/calendars/sinhala_astronomy.py module using Skyfield to compute astronomical events for the Sinhala lunisolar calendar: sidereal longitude, lunar phase/tithi, new and full moon detection, sunrise calculation, Sinhala month assignment, and generation of Poya date records for a given year.

Changes

Sinhala Astronomy Calculations

Layer / File(s) Summary
Module setup and core sidereal helpers
holidays/calendars/sinhala_astronomy.py
Adds module docstring, Skyfield imports, ephemeris/timescale initialization, location and configuration constants, Sinhala month name mapping, and functions for Lahiri ayanamsha, sidereal longitude/sign, lunar phase, and tithi conversion.
New moon detection, sunrise, and month assignment
holidays/calendars/sinhala_astronomy.py
Adds find_surrounding_new_moons, get_sunrise, get_month_info, and get_fullmoon_month to locate bounding new moons, compute sunrise, and derive Sinhala month/leap status from sidereal data and tithi.
Full moon enumeration and Poya date generation
holidays/calendars/sinhala_astronomy.py
Adds find_full_moons to enumerate full moon events per year and generate_poya_dates to produce localized civil date, month, and datetime records for Poya observances.

Estimated code review effort: 4 (Complex) | ~45 minutes

Related issues: #3447 — implements calculation-based Poya day estimation replacing the fixed lookup list approach.

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,
Sidereal signs in Ujjain light.
Tithi counted, new moon found,
Poya dates now safely bound.
Skyfield spins, the stars align —
No more fixed lists, just clean design. 🐇🌕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds the astronomical engine, but it is still standalone and does not replace the fixed-list Sri Lanka holiday logic. Integrate the new astronomical calculations into the Sinhala holiday path and remove the fixed-list/special-case Poya estimation flow.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: a new astronomical Sinhala calendar implementation.
Description check ✅ Passed The description is clearly related to the changeset and matches the new astronomical Sinhala calendar backend.
Out of Scope Changes check ✅ Passed The changes stay focused on the requested Sinhala calendar computation work and do not introduce unrelated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 382e718 and 01bbb8f.

📒 Files selected for processing (1)
  • holidays/calendars/sinhala_astronomy.py

Comment on lines +23 to +25
from skyfield import almanac
from skyfield.api import Star, load, wgs84
from skyfield.data import hipparcos

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.

🩺 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)"
done

Repository: 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"))
PY

Repository: 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)
PY

Repository: 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")
PY

Repository: 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' holidays

Repository: 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])
PY

Repository: 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")
PY

Repository: 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())
PY

Repository: 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.

Comment on lines +27 to +35
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)

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.

🩺 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):

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.

📐 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.

Suggested change
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

Comment on lines +179 to +219
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,
}
)

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.

🎯 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 -n

Repository: 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(),
                )
PY

Repository: 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)
PY

Repository: 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 -n

Repository: 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)
PY

Repository: 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:


🌐 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:


🌐 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:


🌐 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:


🌐 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

3 issues found and verified against the latest diff

Confidence score: 2/5

  • In holidays/calendars/sinhala_astronomy.py, skyfield is imported unconditionally without being declared as a dependency, so clean installs can fail immediately with ModuleNotFoundError and break module usage on import — add skyfield as an explicit, pinned dependency before merging.
  • holidays/calendars/sinhala_astronomy.py currently loads ephemeris/catalog/timescale data at import time, and Skyfield may download files during load(), 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 — adjust find_full_moon_days year 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

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.

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")

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.

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):

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.

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>

@sonarqubecloud

sonarqubecloud Bot commented Jul 3, 2026

Copy link
Copy Markdown


def lahiri_ayanamsha(ts_time):

_, longitude, _ = earth.at(ts_time).observe(SPICA).apparent().ecliptic_latlon("date")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why is Spica here?

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.

Implementation of Sinhala Calendar for Poya Days's future observance estimations

2 participants