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"}
]
endOnly 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
endThen 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— aCalendar.TimeZoneDatabasemodule. Defaults to whichever of:tzor:tzdatais loaded.:time_zone_resolver— a 1-arity function taking aGeo.Pointand returning{:ok, zone_name}or{:error, reason}. Defaults toTzWorld.timezone_at/1.:solar_elevation— how far below the horizon counts as the event. Sun events only; see the Solar guide.:limband:interpolation— which part of the Moon's disk marks the event, and how its position is evaluated. Moon events only; seeAstro.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_worldis not a dependency and no:time_zone_resolverwas given.{:error, :tz_world_data_not_installed}—:tz_worldis installed but its data has never been downloaded. Runmix 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/3andsolstice/3are 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.