GreenCal 🌱🌙
Agricultural sun & moon calendar for Elixir — pure arithmetic, zero dependencies, no time zone database required.
From a geolocation, a date (or a period) and optionally an elevation, GreenCal computes:
- Sun — sunrise, solar noon, sunset, day length, rise/set azimuths, civil / nautical / astronomical twilights (Meeus ch. 25, ~1 s of time)
- Moon — moonrise, moonset, transit, phase, illuminated fraction, distance (full Meeus ch. 47 series, validated against example 47.a to 0.13″ in longitude)
- The three independent lunar cycles used by agricultural calendars: waxing/waning (illumination), ascending/descending (declination — the one biodynamic sowing calendars actually use), perigee/apogee (distance)
- Exact instants of everything a printed calendar marks with a symbol
(
GreenCal.lunar_events/2): phases (with an eclipse screening flag), perigees/apogees, node crossings, and lunar standstills (the exact ascending → descending flips) - The constellation the Moon stands in — equal-sector sidereal zodiac
(Lahiri) by default, or the real unequal IAU boundaries (13
constellations, Ophiuchus included) with
boundaries: :iau, the convention of printed biodynamic calendars — plus its element (fire/earth/air/water) and the traditionally associated plant organ (fruit/root/flower/leaf)
Rise/set times are found by numeric search (sampling + bisection) rather than the closed-form hour-angle formula, so moonrise is accurate too (the closed form can be 15+ minutes off for the Moon) and polar edge cases (midnight sun, polar night, "no moonrise today") are reported explicitly instead of producing garbage. ΔT (TT − UT) uses observed IERS values on 2000–2026, the Espenak & Meeus polynomials before that, and a documented hold-then-bridge extrapolation after — so results don't drift over the years.
Usage
loc = {48.8566, 2.3522} # Paris; {lat, lon}, East positive
day = GreenCal.day(loc, ~D[2026-06-21], elevation: 35.0)
day.sun.rise #=> ~U[2026-06-21 03:45:21Z]
day.sun.day_length_minutes #=> 974.1
day.twilight.dawn #=> ~U[2026-06-21 03:04:16Z]
day.moon.phase #=> :first_quarter
day.moon.trend #=> :descending (declination — sowing calendars)
day.moon.illumination_trend #=> :waxing (independent cycle!)
day.constellation #=> "Virgo"
day.organ #=> :root
# A whole month, with local times (needs a tz database, e.g. tzdata):
GreenCal.calendar(loc, Date.range(~D[2026-07-01], ~D[2026-07-31]),
time_zone: "Europe/Paris")
# A year in ~160 ms:
GreenCal.calendar(loc, Date.range(~D[2026-01-01], ~D[2026-12-31]), parallel: true)
# Exact instants of phases, apsides, nodes and standstills (geocentric):
GreenCal.lunar_events(Date.range(~D[2026-08-01], ~D[2026-08-31]))
# phases: [%{type: :new_moon, at: ~U[2026-08-12 17:36:40Z], eclipse: :likely}, ...]
The low-level astronomy is public too — GreenCal.Astro (UT-facing facade),
GreenCal.Astro.Sun, GreenCal.Astro.Moon, GreenCal.Astro.RiseSet,
GreenCal.Astro.Time — if you need positions, ephemerides or custom events.
An interactive tour lives in livebooks/green_cal.livemd.
Accuracy
| Quantity | Method | Validated against |
|---|---|---|
| Solar position | Meeus ch. 25 (low accuracy) | Example 25.a |
| Lunar position | Meeus ch. 47, full 60+60 term tables | Example 47.a (0.13″ / 0.03 km) |
| Nutation | Meeus ch. 22 short series (~0.5″) | Example 22.a |
| Sidereal time | Meeus ch. 12 | Examples 12.a / 12.b |
| ΔT | Observed IERS 2000–2026, polynomials before, hold-then-bridge after | USNO deltat.data |
| Sun rise/set | numeric search | NOAA-consistent, < 1 min for |lat| < 66° |
| Moon rise/set | numeric search with per-instant parallax h₀ | USNO API (Paris, Sydney, Edinburgh — ≤ 30 s) |
| Phase instants | elongation crossings, bisection | Published 2026 instants (≤ 3 min) |
| Eclipse screening | |β| at syzygy (Meeus ch. 54 criterion) | All 4 eclipses of 2026, no false positive |
| IAU constellations | boundary table (J2000) + precession | Solar entry dates, all 13 constellations |
Honesty note. The astronomy above is science; elements, organs and "sowing days" are tradition. GreenCal computes the former rigorously and labels the latter for what it is.
Known limitations
- Refraction is the fixed standard 34′. Real refraction varies with temperature and pressure; near the horizon at high latitudes this can shift a rise time by several minutes on unusual days. NOAA and USNO tables share this convention.
:elevationmodels an unobstructed horizon below the observer (hilltop, coast, plain). In a valley the visible horizon is higher, not lower — pass0and expect the sun later than computed.- Moon times are geocentric apart from the parallax term absorbed in h₀; the topocentric transit can differ by a few seconds.
- Future ΔT is unknowable. Observed values end in 2026; beyond, the
last value is held (documented in
GreenCal.Astro.Time). Error should stay under a second for ~a decade, then grow slowly. Override with:delta_tif you need better. - Eclipse flags are screening, not circumstances — "an eclipse happens somewhere on Earth", not where or how deep.
Installation
def deps do
[
{:green_cal, "~> 0.1.1"}
]
end
Documentation: https://hexdocs.pm/green_cal.
License
MIT