Agricultural sun & moon calendar — pure Elixir, zero dependencies.
From a geolocation, a date (or a date range) and optionally an elevation, GreenCal computes everything a gardening / market-farming calendar needs:
- sunrise, solar noon, sunset, day length, civil dawn/dusk
- moonrise, moonset, phase, illuminated fraction, distance
- the three independent lunar cycles used by agricultural calendars: waxing/waning (illumination), ascending/descending (declination — the one biodynamic sowing calendars care about), and perigee/apogee (distance)
- lunar node crossings (β = 0), the days biodynamic calendars mark as unfavorable
- the sidereal constellation the Moon stands in, its element and the plant organ traditionally associated with it
Quick start
day = GreenCal.day({48.8566, 2.3522}, ~D[2026-06-21])
day.sun.rise #=> ~U[2026-06-21 03:46:57Z]
day.moon.phase #=> :first_quarter
day.moon.trend #=> :descending
GreenCal.calendar({48.8566, 2.3522}, Date.range(~D[2026-07-01], ~D[2026-07-31]))Options
Accepted by day/3 and calendar/3:
:elevation— meters above sea level (default0.0). Deepens the apparent horizon dip: about one minute of earlier sunrise per 100 m. Applies to sun and moon rise/set only — twilights are defined by the Sun's geometric altitude and are deliberately unaffected (a higher vantage point sees the Sun sooner, but the sky's illumination geometry does not change).:time_zone— IANA zone name (e.g."Europe/Paris"). The civil day then runs from local midnight to local midnight and everyDateTimeis returned in that zone. Requires a configured time zone database (e.g.tzdata); the default"Etc/UTC"needs none.:twilight— altitude used for dawn/dusk::civil(default),:nautical,:astronomical, or degrees.:boundaries— constellation convention::equal_sidereal(default) or:iau(seeconstellation_of/3).:delta_t— override ΔT in seconds (seeGreenCal.Astro.Time).
A warning about the interpretive layer
Rise/set times, phases and declination trends are astronomy: they are computed here to the minute and validated against published references. Elements, organs and "sowing days" are tradition, not science — GreenCal computes the underlying astronomy faithfully and labels the traditional mapping for what it is.
Note on constellations: the default is the sidereal zodiac with a Lahiri
ayanamsa and equal 30° sectors. Printed biodynamic calendars (Maria Thun
et al.) use the unequal IAU constellation boundaries instead — pass
boundaries: :iau to match them (13 sectors, Ophiuchus included).
Summary
Types
Latitude and longitude in degrees, East positive.
Functions
One GreenCal.Day per date of the range (or any enumerable of dates).
Constellation occupied by a tropical ecliptic longitude.
Everything about one civil day at one location.
Geocentric lunar events over a date range — no location involved.
Types
Functions
@spec calendar(location(), Enumerable.t(), keyword()) :: [GreenCal.Day.t()]
One GreenCal.Day per date of the range (or any enumerable of dates).
Days are independent computations (~1.8 ms each): pass parallel: true
to spread them over the schedulers with Task.async_stream/3 — a full
year drops from ~630 ms to ~160 ms on a typical machine. Order is
preserved either way, and exceptions stay rescuable in both modes.
GreenCal.calendar(loc, Date.range(~D[2026-07-01], ~D[2026-07-31]))
GreenCal.calendar(loc, Date.range(~D[2026-01-01], ~D[2026-12-31]), parallel: true)
Constellation occupied by a tropical ecliptic longitude.
Two boundary conventions, chosen with the :boundaries option:
:equal_sidereal(default) — sidereal zodiac, twelve equal 30° sectors, Lahiri ayanamsa. The convention of Indian ephemerides.:iau— the real (unequal) IAU constellation boundaries along the ecliptic, thirteen sectors including Ophiuchus. This is what printed biodynamic calendars (Maria Thun et al.) use; Ophiuchus carries:water, as those calendars fold it into Scorpius.
Both drift together against the tropical zodiac by ~50.3″/yr (precession), computed continuously — no jump at January 1st.
Returns {name, element}.
iex> GreenCal.constellation_of(45.0, ~D[2026-01-01])
{"Aries", :fire}
iex> GreenCal.constellation_of(250.0, ~D[2026-01-01], boundaries: :iau)
{"Ophiuchus", :water}
@spec day(location(), Date.t(), keyword()) :: GreenCal.Day.t()
Everything about one civil day at one location.
See GreenCal.Day for the returned struct and the module doc for the
option list.
The :sun and :moon structs carry a :state field: :normal, or
:always_above / :always_below when the body never crosses the horizon
that day (polar day/night for the Sun; for the Moon this legitimately
happens about one day per month — a nil moonrise is not a bug).
@spec lunar_events( Date.Range.t(), keyword() ) :: %{phases: [map()], apsides: [map()], nodes: [map()], standstills: [map()]}
Geocentric lunar events over a date range — no location involved.
Everything a printed lunar calendar marks with a symbol, as exact instants:
:phases— new moon, quarters, full moon, with the:eclipsescreening flag (seeGreenCal.Astro.phase_events/3):apsides— perigees and apogees, with distances:nodes— ascending / descending node crossings:standstills— northernmost / southernmost declination, i.e. the exact flips between ascending and descending Moon
Times are UTC DateTimes, or local ones with the :time_zone option.
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}, ...],
# apsides: [...], nodes: [...], standstills: [...]}