Documentation
¶
Overview ¶
Package dusk provides astronomical calculations: twilight times, sunrise/sunset, moonrise/moonset, and lunar phase.
All angles are in degrees. Time parameters use time.Time. Functions that produce local times accept an Observer with a timezone.
Two sentinel errors distinguish polar edge cases: ErrCircumpolar (object always above the horizon) and ErrNeverRises (object never rises).
Zero-value time.Time in result structs signals "event did not occur" for a specific day (e.g., the Moon rises but does not set before midnight). Check with time.Time.IsZero. This is distinct from sentinel errors, which indicate the geometry makes the event impossible at the given latitude.
References ¶
- Meeus, Jean. Astronomical Algorithms. 2nd ed. Willmann-Bell, 1998.
Index ¶
Examples ¶
Constants ¶
const ErrCircumpolar = errString("dusk: object is circumpolar (always above the horizon)")
ErrCircumpolar is returned when a celestial object is circumpolar (always above the horizon) at the given latitude.
const ErrDateOutOfRange = errString("dusk: date outside valid range (1677-09-21 to 2262-04-11)")
ErrDateOutOfRange is returned when a date falls outside the valid range for Julian date calculations (the int64 nanosecond bounds, approximately 1677-09-21 to 2262-04-11).
const ErrInvalidCoord = errString("dusk: latitude must be in [-90, 90] and longitude in [-180, 180]")
ErrInvalidCoord is returned when latitude or longitude are outside the valid range in NewObserver.
const ErrNeverRises = errString("dusk: object never rises at this latitude")
ErrNeverRises is returned when a celestial object never rises above the horizon at the given latitude.
const ErrNilLocation = errString("dusk: location must not be nil")
ErrNilLocation is returned when a nil *time.Location is passed to NewObserver.
const ErrNonFiniteCoord = errString("dusk: coordinates must be finite (NaN and Inf are not allowed)")
ErrNonFiniteCoord is returned when NaN or Inf coordinates are passed to NewObserver.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type LunarPhaseInfo ¶
type LunarPhaseInfo struct {
Illumination float64 // percentage 0-100
Elongation float64 // degrees 0-360
Angle float64 // phase angle in degrees (may be negative per Meeus formula)
DaysApprox float64 // rough days into lunation (linear estimate from elongation)
Waxing bool // true from New Moon to Full Moon (elongation 0-180)
Name string // "New Moon", "Waxing Crescent", etc.
}
LunarPhaseInfo describes the Moon's current phase.
func LunarPhase ¶
func LunarPhase(date time.Time) (LunarPhaseInfo, error)
LunarPhase returns the lunar phase at the given instant.
Unlike SunriseSunset and MoonriseMoonset which use only the calendar date, LunarPhase uses the exact time — the phase changes continuously. The result depends on the UTC instant; timezone does not affect the calculation.
The phase angle uses the Meeus approach: solar ecliptic longitude from the mean-anomaly method, lunar ecliptic position from Chapter 47 tables.
An error is returned if the date is out of the valid Julian date range.
Example ¶
package main
import (
"fmt"
"time"
"github.com/philoserf/dusk/v3"
)
func main() {
date := time.Date(2024, 1, 25, 18, 0, 0, 0, time.UTC)
phase, err := dusk.LunarPhase(date)
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
fmt.Printf("Phase: %s\n", phase.Name)
fmt.Printf("Illumination: %.0f%%\n", phase.Illumination)
}
Output: Phase: Full Moon Illumination: 100%
func (LunarPhaseInfo) String ¶
func (l LunarPhaseInfo) String() string
String returns a human-readable representation of the lunar phase.
type MoonEvent ¶
type MoonEvent struct {
Rise time.Time // zero value if the Moon does not rise
Set time.Time // zero value if the Moon does not set
AboveHorizon bool // true if Moon was above the horizon at start of day
}
MoonEvent holds the rise and set times for the Moon on a given day, along with the duration between rise and set.
func MoonriseMoonset ¶
MoonriseMoonset computes the moonrise and moonset times for the given date at the specified observer position and timezone. The date is converted to the observer's timezone to determine the local calendar day, then the function scans that local day (midnight to midnight) for rise/set events. This means the same time.Time can produce different results for observers in different timezones.
The algorithm scans minute-by-minute through the day to detect altitude sign changes. This is slow by design (~1440 ecliptic-position evaluations). A single call takes approximately 1-2 ms on modern hardware (Apple M-series or equivalent; see BenchmarkMoonriseMoonset). Callers computing moonrise/moonset for many dates (e.g., a 30-day calendar ≈ 30-60 ms) should expect proportional cost and may benefit from caching or parallelization.
The one-minute resolution means events shorter than one minute may not be detected. At polar or near-polar latitudes, the Moon can graze the horizon briefly enough to fall within a single scan step.
An error is returned if the date is out of the valid Julian date range.
Example ¶
package main
import (
"fmt"
"time"
"github.com/philoserf/dusk/v3"
)
func main() {
loc, err := time.LoadLocation("America/New_York")
if err != nil {
fmt.Println("error:", err)
return
}
date := time.Date(2024, 1, 15, 0, 0, 0, 0, loc)
obs, err := dusk.NewObserver(40.7128, -74.0060, loc)
if err != nil {
fmt.Println("error:", err)
return
}
evt, err := dusk.MoonriseMoonset(date, obs)
if err != nil {
fmt.Println("error:", err)
return
}
if !evt.Rise.IsZero() {
fmt.Printf("Moonrise: %s\n", evt.Rise.Format("15:04"))
}
if !evt.Set.IsZero() {
fmt.Printf("Moonset: %s\n", evt.Set.Format("15:04"))
}
}
Output: Moonrise: 10:06 Moonset: 22:13
type Observer ¶
type Observer struct {
// contains filtered or unexported fields
}
Observer represents a geographic position on Earth used as the viewpoint for all astronomical calculations.
func NewObserver ¶
NewObserver constructs an Observer after validating all inputs. lat must be in [-90, 90], lon in [-180, 180], and loc must not be nil. NaN and infinite values are rejected.
Example ¶
package main
import (
"fmt"
"math"
"time"
"github.com/philoserf/dusk/v3"
)
func main() {
// Valid observer
obs, err := dusk.NewObserver(40.7128, -74.006, time.UTC)
if err != nil {
fmt.Println("unexpected error:", err)
return
}
fmt.Println(obs)
// Invalid: latitude out of range
_, err = dusk.NewObserver(91, 0, time.UTC)
fmt.Println(err)
// Invalid: NaN
_, err = dusk.NewObserver(math.NaN(), 0, time.UTC)
fmt.Println(err)
}
Output: 40.7128°, -74.0060° (UTC) dusk: latitude must be in [-90, 90] and longitude in [-180, 180] dusk: coordinates must be finite (NaN and Inf are not allowed)
type SunEvent ¶
SunEvent holds the times of sunrise, solar noon, sunset, and the duration of daylight for a single day.
func SunriseSunset ¶
SunriseSunset computes sunrise, solar noon, and sunset for the given date and observer position. The observer must be constructed via NewObserver. The date is converted to the observer's timezone to determine the local calendar day; the time-of-day is ignored. Output times are converted to the observer's timezone.
The algorithm follows the NOAA solar calculator method (derived from Meeus, Astronomical Algorithms).
Example ¶
package main
import (
"fmt"
"time"
"github.com/philoserf/dusk/v3"
)
func main() {
loc, err := time.LoadLocation("America/Chicago")
if err != nil {
fmt.Println("error:", err)
return
}
date := time.Date(2025, 6, 21, 0, 0, 0, 0, loc)
obs, err := dusk.NewObserver(42.9634, -85.6681, loc)
if err != nil {
fmt.Println("error:", err)
return
}
sun, err := dusk.SunriseSunset(date, obs)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("Sunrise: %s\n", sun.Rise.Format("15:04"))
fmt.Printf("Sunset: %s\n", sun.Set.Format("15:04"))
}
Output: Sunrise: 05:03 Sunset: 20:25
Example (Polar) ¶
package main
import (
"errors"
"fmt"
"time"
"github.com/philoserf/dusk/v3"
)
func main() {
loc, err := time.LoadLocation("Europe/Oslo")
if err != nil {
fmt.Println("error:", err)
return
}
obs, err := dusk.NewObserver(69.65, 18.96, loc)
if err != nil {
fmt.Println("error:", err)
return
}
// Tromsø on June 21 — midnight sun
date := time.Date(2024, 6, 21, 0, 0, 0, 0, loc)
_, err = dusk.SunriseSunset(date, obs)
if errors.Is(err, dusk.ErrCircumpolar) {
fmt.Println("Midnight sun — no sunrise or sunset")
}
// Tromsø on December 21 — polar night
date = time.Date(2024, 12, 21, 0, 0, 0, 0, loc)
_, err = dusk.SunriseSunset(date, obs)
if errors.Is(err, dusk.ErrNeverRises) {
fmt.Println("Polar night — sun never rises")
}
}
Output: Midnight sun — no sunrise or sunset Polar night — sun never rises
type TwilightEvent ¶
type TwilightEvent struct {
Dusk time.Time // evening boundary (today)
Dawn time.Time // morning boundary (tomorrow)
NightDuration time.Duration // time from Dusk to Dawn (overnight darkness)
}
TwilightEvent holds the dusk and dawn times of a twilight period. Dusk is tonight's boundary (sun passes below the depression angle). Dawn is tomorrow morning's boundary (sun passes above the depression angle). To get this morning's dawn, call with yesterday's date.
func AstronomicalTwilight ¶
func AstronomicalTwilight(date time.Time, obs Observer) (TwilightEvent, error)
AstronomicalTwilight computes the evening astronomical twilight period (Sun 18 degrees below the horizon) for the given date and observer position.
func CivilTwilight ¶
func CivilTwilight(date time.Time, obs Observer) (TwilightEvent, error)
CivilTwilight computes the evening civil twilight period (Sun 6 degrees below the horizon) for the given date and observer position. Dusk is tonight's civil dusk; Dawn is tomorrow morning's civil dawn.
Example ¶
package main
import (
"fmt"
"time"
"github.com/philoserf/dusk/v3"
)
func main() {
loc, err := time.LoadLocation("America/Los_Angeles")
if err != nil {
fmt.Println("error:", err)
return
}
date := time.Date(2025, 6, 21, 0, 0, 0, 0, loc)
obs, err := dusk.NewObserver(47.6062, -122.3321, loc)
if err != nil {
fmt.Println("error:", err)
return
}
tw, err := dusk.CivilTwilight(date, obs)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("Dusk: %s\n", tw.Dusk.Format("15:04"))
fmt.Printf("Dawn: %s\n", tw.Dawn.Format("15:04"))
}
Output: Dusk: 21:51 Dawn: 04:31
func NauticalTwilight ¶
func NauticalTwilight(date time.Time, obs Observer) (TwilightEvent, error)
NauticalTwilight computes the evening nautical twilight period (Sun 12 degrees below the horizon) for the given date and observer position.
func (TwilightEvent) String ¶
func (tw TwilightEvent) String() string
String returns a human-readable representation of the twilight event.