GreenCal.Day (GreenCal v0.2.0)

Copy Markdown View Source

One computed civil day — the return type of GreenCal.day/3.

All DateTime fields are UTC unless a :time_zone option was given. A nil event time means the event does not occur within the civil day; check the matching :state field to know why.

Instants versus scalars

The day holds two kinds of value, and they are not sampled the same way.

Instantssun.rise, twilight.dawn, moon.set, moon.node.at, moon.phase_change.at … — are exact: each is searched for within the civil day and reported to the second.

Scalars are a snapshot taken at sampled_at, the middle of the civil day — local noon on a 24 h day, half an hour off it on the two DST days a year that run 23 or 25 hours. That covers every number and label the day carries: moon.phase, moon.elongation, moon.illuminated_fraction, moon.distance_km, moon.declination, moon.ecliptic_latitude, the three *_trend fields (computed from ±12 h around sampled_at), and constellation / element / organ.

So illuminated_fraction displayed at 23:00 is the value it had at noon, not at 23:00 — the Moon's illumination moves by about 1.5 points over a day. For a value at an arbitrary instant, call GreenCal.Astro.phase/2 or GreenCal.Astro.moon/2 directly.

:events — the day's chronology

:events is the ordered view of the instants this struct already holds, not a second computation. Every entry has a canonical home in a field (sun.rise, twilight.dusk, moon.node, moon.apsis…) and the list is a projection of those fields, so the two cannot drift apart.

for e <- day.events, do: {e.family, e.type, e.at}
#=> [{:twilight, :dawn, ...}, {:sun, :rise, ...}, {:moon, :set, ...},
#    {:phase, :full_moon, ...}, {:sun, :set, ...}, ...]

Its contract:

  • Sorted by :at, ascending. Guaranteed, not incidental.
  • Uniform shape: every entry is exactly %{family: atom, type: atom, at: DateTime.t()} — no optional keys, ever. The azimuth of a sunrise, the distance of a perigee, the declination of a standstill live in the structured field, which is made for them.
  • Same time zone as the rest of the struct.
  • Always present. There is no option to skip it, so [] means the day holds nothing, never "not computed".

An absent event is not a non-event

A missing {:sun, :rise} entry can mean polar day or polar night, and the flat list cannot tell you which — sun.state can (:always_above / :always_below), and so can twilight.state and moon.state. Reach for :state before rendering a dash: above the Arctic circle in June the honest label is "the Sun does not set", not "—".

Being a projection, :events also inherits what the struct chooses to report: on the rare high-latitude day holding two moonrises, the struct keeps one, and so does the list.

Families: topocentric and geocentric

FamilyTypes
Topocentric — depends on the location:sun:rise, :transit, :set
:twilight:dawn, :dusk
:moon:rise, :transit, :set
Geocentric — the same everywhere on Earth:phase:new_moon, :first_quarter, :full_moon, :last_quarter
:apsis:perigee, :apogee
:node:ascending, :descending
:standstill:northernmost, :southernmost

The split is worth knowing when you compute several places at once: over a set of plots on the same farm, only the topocentric families change. The geocentric ones are the same instants for everyone — compute them once with GreenCal.lunar_timeline/2 rather than once per plot.

Summary

Types

One entry of GreenCal.Day :events — see the module doc for the families and their types, and for why the shape carries nothing else.

t()

Types

event()

@type event() :: %{family: atom(), type: atom(), at: DateTime.t()}

One entry of GreenCal.Day :events — see the module doc for the families and their types, and for why the shape carries nothing else.

t()

@type t() :: %GreenCal.Day{
  constellation: String.t(),
  date: Date.t(),
  element: :fire | :earth | :air | :water,
  events: [event()],
  location: GreenCal.location(),
  moon: GreenCal.Day.Moon.t(),
  organ: :fruit | :root | :flower | :leaf,
  sampled_at: DateTime.t(),
  sun: GreenCal.Day.Sun.t(),
  twilight: GreenCal.Day.Twilight.t()
}