GreenCal 🌱🌙

CI

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:

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
# Everything that happens that day, in order — one uniform shape, no
# optional keys, already sorted:
for e <- day.events, do: {e.family, e.type, e.at}
#=> [{:twilight, :dawn, ...}, {:sun, :rise, ...}, {:moon, :transit, ...},
# {:phase, :first_quarter, ...}, {:sun, :set, ...}, ...]
# 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")
# Spread a year over the schedulers:
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_timeline(Date.range(~D[2026-08-01], ~D[2026-08-31]))
#=> [%{family: :phase, type: :last_quarter, at: ~U[2026-08-06 02:21:49Z],
# eclipse: :none}, ...]

The seven event families split along a line worth knowing: :sun, :twilight and :moon are topocentric (they depend on where you stand), while :phase, :apsis, :node and :standstill are geocentric — the same instants everywhere on Earth. Computing several places at once, only the first three change: get the rest once from GreenCal.lunar_timeline/2 instead of once per place.

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

QuantityMethodValidated against
Solar positionMeeus ch. 25 (low accuracy)Example 25.a
Lunar positionMeeus ch. 47, full 60+60 term tablesExample 47.a (0.13″ / 0.03 km)
NutationMeeus ch. 22 short series (~0.5″)Example 22.a
Sidereal timeMeeus ch. 12Examples 12.a / 12.b
ΔTObserved IERS 2000–2026, polynomials before, hold-then-bridge afterUSNO deltat.data
Sun rise/setnumeric searchNOAA-consistent, < 1 min for |lat| < 66°
Moon rise/setnumeric search with per-instant parallax h₀USNO API (Paris, Sydney, Edinburgh — ≤ 30 s)
Phase instantselongation crossings, bisectionPublished 2026 instants (≤ 3 min)
Eclipse screening|β| at syzygy (Meeus ch. 54 criterion)All 4 eclipses of 2026, no false positive
IAU constellationsboundary table (J2000) + precessionSolar 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

Installation

def deps do
[
{:green_cal, "~> 0.2.0"}
]
end

Documentation: https://hexdocs.pm/green_cal.

License

MIT