Skip to content

Repository files navigation

dusk

CI Go Reference

A single, zero-dependency Go package for astronomical calculations — sunrise/sunset, moonrise/moonset, twilight, and lunar phase — based on Meeus's Astronomical Algorithms.

Install

go get github.com/philoserf/dusk/v3

Examples

Sunrise and sunset

A complete program showing error handling and formatted output:

package main

import (
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/philoserf/dusk/v3"
)

func main() {
	loc, err := time.LoadLocation("America/Chicago")
	if err != nil {
		log.Fatal(err)
	}

	obs, err := dusk.NewObserver(42.9634, -85.6681, loc)
	if err != nil {
		log.Fatal(err)
	}

	date := time.Date(2025, 6, 21, 0, 0, 0, 0, time.UTC)

	sun, err := dusk.SunriseSunset(date, obs)
	if err != nil {
		if errors.Is(err, dusk.ErrCircumpolar) {
			fmt.Println("Midnight sun — the sun does not set today.")
			return
		}
		if errors.Is(err, dusk.ErrNeverRises) {
			fmt.Println("Polar night — the sun does not rise today.")
			return
		}
		log.Fatal(err)
	}

	fmt.Printf("Sunrise:  %s\n", sun.Rise.Format(time.Kitchen))
	fmt.Printf("Noon:     %s\n", sun.Noon.Format(time.Kitchen))
	fmt.Printf("Sunset:   %s\n", sun.Set.Format(time.Kitchen))
	fmt.Printf("Daylight: %s\n", sun.Duration)
}

Moonrise and moonset

The Moon may not rise or set on a given day. Use IsZero() to check, and AboveHorizon to determine whether the Moon was up at the start of the day:

moon, err := dusk.MoonriseMoonset(date, obs)
if err != nil {
	log.Fatal(err)
}

switch {
case moon.Rise.IsZero() && moon.Set.IsZero():
	if moon.AboveHorizon {
		fmt.Println("Moon is above the horizon all day.")
	} else {
		fmt.Println("Moon is below the horizon all day.")
	}
case moon.Rise.IsZero():
	fmt.Println("Moon was already up at midnight.")
	fmt.Printf("Moonset:  %s\n", moon.Set.Format(time.Kitchen))
case moon.Set.IsZero():
	fmt.Printf("Moonrise: %s\n", moon.Rise.Format(time.Kitchen))
	fmt.Println("Moon stays up past midnight.")
default:
	fmt.Printf("Moonrise: %s\n", moon.Rise.Format(time.Kitchen))
	fmt.Printf("Moonset:  %s\n", moon.Set.Format(time.Kitchen))
}

Lunar phase

All result types implement fmt.Stringer. Printing a LunarPhaseInfo value directly produces output like Waxing Gibbous 67.3% (day 10.1):

phase, err := dusk.LunarPhase(time.Date(2024, 1, 18, 3, 0, 0, 0, time.UTC))
if err != nil {
	log.Fatal(err)
}

fmt.Println(phase) // e.g., "Waxing Gibbous 67.3% (day 10.1)"
fmt.Printf("Illumination: %.1f%%  Waxing: %t\n", phase.Illumination, phase.Waxing)

Civil twilight

Twilight functions return tonight's Dusk and tomorrow morning's Dawn. To get this morning's dawn, call with yesterday's date:

loc, err := time.LoadLocation("America/Los_Angeles")
if err != nil {
	log.Fatal(err)
}

obs, err := dusk.NewObserver(47.6062, -122.3321, loc)
if err != nil {
	log.Fatal(err)
}

date := time.Date(2025, 6, 21, 0, 0, 0, 0, time.UTC)

tw, err := dusk.CivilTwilight(date, obs)
if err != nil {
	log.Fatal(err)
}

fmt.Printf("Dusk:           %s\n", tw.Dusk.Format(time.Kitchen))
fmt.Printf("Dawn:           %s\n", tw.Dawn.Format(time.Kitchen))
fmt.Printf("Night duration: %s\n", tw.NightDuration)

NauticalTwilight and AstronomicalTwilight follow the same signature.

Polar error handling

At extreme latitudes, sunrise/sunset and twilight may be geometrically impossible. Use errors.Is to match the sentinel errors:

loc, err := time.LoadLocation("Arctic/Longyearbyen")
if err != nil {
	log.Fatal(err)
}

obs, err := dusk.NewObserver(78.2, 15.6, loc) // Svalbard
if err != nil {
	log.Fatal(err)
}

midsummer := time.Date(2025, 6, 21, 0, 0, 0, 0, time.UTC)

_, err = dusk.SunriseSunset(midsummer, obs)
if errors.Is(err, dusk.ErrCircumpolar) {
	fmt.Println("Midnight sun — no sunset at this latitude today.")
}
if errors.Is(err, dusk.ErrNeverRises) {
	fmt.Println("Polar night — no sunrise at this latitude today.")
}

API

Solar

  • SunriseSunset(date, obs) — sunrise, solar noon, sunset, and daylight duration

Lunar

  • MoonriseMoonset(date, obs) — moonrise/moonset times and whether the Moon was above the horizon at the start of the day
  • LunarPhase(date) — illumination, elongation, approximate age, waxing/waning, phase angle, and name

Twilight

  • CivilTwilight(date, obs) — sun 6 degrees below horizon
  • NauticalTwilight(date, obs) — sun 12 degrees below horizon
  • AstronomicalTwilight(date, obs) — sun 18 degrees below horizon

Observer

  • NewObserver(lat, lon, loc) — create a validated observer from latitude, longitude, and timezone

Result types

All result types implement fmt.Stringer:

  • SunEventRise, Noon, Set times and Duration (daylight)
  • MoonEventRise, Set times and AboveHorizon
  • TwilightEventDusk, Dawn times and NightDuration (overnight darkness)
  • LunarPhaseInfoIllumination, Elongation, Angle, DaysApprox, Waxing, Name

Errors

  • ErrCircumpolar — object always above the horizon (e.g., midnight sun)
  • ErrNeverRises — object never rises (e.g., polar night)
  • ErrNilLocation — nil timezone passed to NewObserver
  • ErrNonFiniteCoord — NaN or Inf coordinates
  • ErrInvalidCoord — latitude or longitude out of range
  • ErrDateOutOfRange — date outside supported Julian date range (~1677–2262)

Conventions

  • All angles are in degrees.
  • Longitude is east-positive, west-negative (e.g., New York is -74.006).
  • Observer is constructed via NewObserver, which validates coordinates and rejects NaN/Inf.
  • Functions that can fail return error. Two sentinel errors distinguish polar edge cases: ErrCircumpolar and ErrNeverRises.
  • A zero-value time.Time signals "event did not occur" (e.g., the Moon does not rise on a given day). Check with .IsZero().
  • Twilight functions return tonight's Dusk and tomorrow morning's Dawn. To get this morning's dawn, call with yesterday's date.

Accuracy

Sunrise/sunset times are typically within 1-2 minutes of USNO data. Moonrise/moonset uses a simplified Meeus approach with a minute-by-minute altitude scan and can differ from USNO by up to ~20 minutes. Lunar phase illumination is within 1-2% of published values. Lunar ecliptic position uses the full Meeus Chapter 47 periodic terms (100+ coefficients).

Requirements

Go 1.24+. Zero dependencies.

License

GPL-3.0. See LICENSE.

Originally created by observerly. This fork includes bug fixes, algorithm improvements, and a complete rewrite.

About

Zero-dependency Go package for astronomical calculations: sunrise/sunset, moonrise/moonset, twilight, and lunar phase, based on Meeus.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Contributors

Languages