Astro computes the position of the sun and moon, and the events that follow from them: sunrise and sunset, moonrise and moonset, twilight, daylight hours, lunar phases, equinoxes and solstices.

Positions come from JPL's DE440s ephemeris rather than from a closed-form approximation, so results agree with published almanacs to within a second or two over the supported date range.

Installation

Add astro to your dependencies:

def deps do
  [
    {:astro, "~> 2.6"},
    {:tz, "~> 0.28"},
    {:tz_world, "~> 2.4"}
  ]
end

Only astro is required. The other two are optional and cover two separate concerns, described below.

The ephemeris

A compact ephemeris covering 1900 to 2100 ships inside the package, so Astro works immediately after mix deps.get with no download step.

If you need dates outside that range, download the full JPL DE440s kernel, which covers 1849 to 2150:

mix astro.download_ephemeris

The downloaded file takes precedence over the bundled one when present. To keep it somewhere specific, configure the path:

config :astro,
  ephemeris: "/path/to/de440s.bsp"

Setting an explicit path is worth doing in production. The default cache location falls back to the system temporary directory when the user has no writable home directory — common for service accounts in containers — and that is ephemeral.

Time zones

Astro answers two different questions about time zones, and they need different things from you.

Converting an instant to a named zone requires a Calendar.TimeZoneDatabase. Add either :tz or :tzdata. Astro prefers Tz.TimeZoneDatabase when both are loaded, and the :elixir :time_zone_database configuration always wins if you set it.

Working out which zone a coordinate falls in requires :tz_world, plus its backend in your supervision tree:

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      TzWorld.Backend.SpatialIndex
    ]

    Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
  end
end

Then install its data, once:

mix tz_world.update --force

--force is needed on a first install because the package ships no data directory for the download to land in.

Without :tz_world you can still use every function — pass time_zone: :utc, or name a zone explicitly, or supply your own :time_zone_resolver.

Locations

A location is a longitude/latitude pair, in that order. Longitude first is the GeoJSON convention and catches people out, so it is worth stating plainly: east is positive, north is positive.

Three forms are accepted, and they are equivalent:

iex> {:ok, datetime} = Astro.sunrise({151.20666584, -33.8559799094}, ~D[2019-12-04])
iex> datetime
#DateTime<2019-12-04 05:37:08.672884+11:00 AEDT Australia/Sydney>

iex> {:ok, datetime} = Astro.sunrise(%Geo.Point{coordinates: {151.20666584, -33.8559799094}}, ~D[2019-12-04])
iex> datetime
#DateTime<2019-12-04 05:37:08.672884+11:00 AEDT Australia/Sydney>

Geo.PointZ also works and carries an altitude, which affects rise and set times slightly.

Options

sunrise/3, sunset/3, moonrise/3 and moonset/3 share a keyword list:

  • :time_zone — :default (the zone at the location, the default), :utc, or any zone name your time zone database knows.

  • :time_zone_database — a Calendar.TimeZoneDatabase module. Defaults to whichever of :tz or :tzdata is loaded.

  • :time_zone_resolver — a 1-arity function taking a Geo.Point and returning {:ok, zone_name} or {:error, reason}. Defaults to TzWorld.timezone_at/1.

  • :solar_elevation — how far below the horizon counts as the event. Sun events only; see the Solar guide.

  • :limb and :interpolation — which part of the Moon's disk marks the event, and how its position is evaluated. Moon events only; see Astro.moonrise/3.

The defaults in force are:

iex> Astro.default_options()
[solar_elevation: 90.0, time_zone: :default, time_zone_database: Tz.TimeZoneDatabase]

Asking for UTC avoids the coordinate-to-zone lookup entirely, so it works with no :tz_world installed:

iex> Astro.sunrise({151.20666584, -33.8559799094}, ~D[2019-12-04], time_zone: :utc)
{:ok, ~U[2019-12-04 18:37:05.706214Z]}

Note this is the sunrise falling inside the UTC day, which near a date boundary is a different event from the one inside the local day.

Errors

Astro returns tagged tuples for the cases that are genuine astronomy, or a setting it cannot use, rather than bad input:

  • {:error, :no_time} — the event does not occur on that date at that place. A polar summer has no sunset:

    iex> Astro.sunset({-62.3481, 82.5018}, ~D[2019-07-01])
    {:error, :no_time}
  • {:error, :not_found} — the date lies outside the loaded ephemeris. Download the full kernel to widen the range.

  • {:error, :time_zone_not_found} — the coordinate is in no time zone, typically open ocean, or the requested zone is unknown to the time zone database.

  • {:error, :time_zone_not_resolved} — :tz_world is not a dependency and no :time_zone_resolver was given.

  • {:error, :tz_world_data_not_installed} — :tz_world is installed but its data has never been downloaded. Run mix tz_world.update --force.

  • {:error, :utc_only_time_zone_database} — a result was asked for in a local time zone, but no time zone database is configured.

  • {:error, :invalid_solar_elevation}, {:error, :invalid_limb} and {:error, :invalid_interpolation} — an option has a value the function does not recognise.

  • {:error, :year_out_of_range} — equinox/3 and solstice/3 are documented for 1000 CE to 3000 CE.

Where next

  • Solar guide — sunrise, sunset, twilight, daylight, equinoxes and solstices.

  • Lunar guide — moonrise, moonset, phases, illumination and crescent visibility.