~gjabell/dawn2dusk

9719c732285d9327ba07b1803509586d721bf811 — Galen Abell 2 years ago
Initial implementation
8 files changed, 201 insertions(+), 0 deletions(-)

A .gitignore
A Makefile
A README.md
A cmd/dawn2dusk/main.go
A go.mod
A go.sum
A lib.go
A lib_test.go
A  => .gitignore +1 -0
@@ 1,1 @@
dawn2dusk

A  => Makefile +15 -0
@@ 1,15 @@
GO := go

GOSRC := $(shell find . -type f -name '*.go')
GOSRC += go.sum go.mod

dawn2dusk: $(GOSRC)
	$(GO) build ./cmd/$@

test: $(GOSRC)
	$(GO) test ./...

clean:
	rm -f dawn2dusk

.PHONY: test clean

A  => README.md +33 -0
@@ 1,33 @@
# dawn2dusk

An offline CLI and library for calculating today's sunrise and sunset times.

## Installing

Requires Go.

```sh
git clone https://git.sr.ht/~gjabell/dawn2dusk && cd dawn2dusk
go install ./...
```

## Usage

```sh
$ dawn2dusk -h
Usage of dawn2dusk:
  -L float
    	Longitude in degrees; must be between -180° and 180° (default -200)
  -l float
    	Latitude in degrees; must be between 0° and 90° (default -1)
  -t string
    	Timezone used for formatting times (default "Local")
```

## Example

```sh
$ dawn2dusk -l 52.5 -L 13.4  # sunrise and sunset in Berlin
SUNRISE: 07:44
SUNSET: 17:56
```

A  => cmd/dawn2dusk/main.go +39 -0
@@ 1,39 @@
package main

import (
	"flag"
	"fmt"
	"git.sr.ht/~gjabell/dawn2dusk"
	"os"
	"time"
)

var lat = flag.Float64("l", -1,
	"Latitude in degrees; must be between 0° and 90°")
var lon = flag.Float64("L", -200,
	"Longitude in degrees; must be between -180° and 180°")
var timeZone = flag.String("t", "Local",
	"Timezone used for formatting times")

func main() {
	flag.Parse()

	if *lat < 0.0 || *lat > 90.0 ||
		*lon < -180.0 || *lon > 180.0 {
		flag.Usage()
		os.Exit(1)
	}

	tz, err := time.LoadLocation(*timeZone)
	if err != nil {
		fmt.Printf("Invalid timezone: %s\n", err)
		os.Exit(1)
	}

	now := time.Now()
	sunrise, sunset := dawn2dusk.CalculateSunriseAndSunset(*lat, *lon, now)

	fmt.Printf("SUNRISE: %s\nSUNSET: %s\n",
		sunrise.In(tz).Format("15:04"),
		sunset.In(tz).Format("15:04"))
}

A  => go.mod +3 -0
@@ 1,3 @@
module git.sr.ht/~gjabell/dawn2dusk

go 1.17

A  => go.sum +0 -0
A  => lib.go +90 -0
@@ 1,90 @@
package dawn2dusk

import (
	"math"
	"time"
)

var zenith = degreesToRadians(90.833)

// CalculateSunriseAndSunset returns the sunrise and sunset times for the given
// latitude and longitude on the given day.
func CalculateSunriseAndSunset(lat, lon float64, dt time.Time) (*time.Time,
	*time.Time) {
	fy := calculateFractionalYear(&dt)
	ha := calculateHourAngle(lat, fy)
	eqtime := calculateEquationOfTime(fy)

	sunrise := int(math.Floor(720 - 4*(lon+ha) - eqtime))
	sunset := int(math.Floor(720 - 4*(lon-ha) - eqtime))

	return timeWithMinutes(&dt, sunrise), timeWithMinutes(&dt, sunset)
}

func timeWithMinutes(dt *time.Time, minutes int) *time.Time {
	t := time.Date(dt.Year(), dt.Month(), dt.Day(),
		0, minutes, 0, 0, time.UTC)
	return &t
}

// calculateHourAngle in degrees.
func calculateHourAngle(lat float64, fractionalYear float64) float64 {
	decl := calculateSolarDeclinationAngle(fractionalYear)
	latitudeRadians := degreesToRadians(lat)
	ha := math.Acos(
		math.Cos(zenith)/math.Cos(latitudeRadians)*math.Cos(decl) -
			math.Tan(latitudeRadians)*math.Tan(decl))

	return radiansToDegrees(ha)
}

// calculateEquationOfTime in radians.
func calculateEquationOfTime(fractionalYear float64) float64 {
	return 229.18 *
		(0.000075 + 0.001868*math.Cos(fractionalYear) -
			0.032077*math.Sin(fractionalYear) -
			0.014615*math.Cos(2*fractionalYear) -
			0.040849*math.Sin(2*fractionalYear))
}

// calculateSolarDeclinationAngle in radians.
func calculateSolarDeclinationAngle(fractionalYear float64) float64 {
	return 0.006918 -
		0.399912*math.Cos(fractionalYear) +
		0.070257*math.Sin(fractionalYear) -
		0.006758*math.Cos(2*fractionalYear) +
		0.000907*math.Sin(2*fractionalYear) -
		0.002697*math.Cos(3*fractionalYear) +
		0.00148*math.Sin(3*fractionalYear)
}

// calculateFractionalYear in radians.
func calculateFractionalYear(now *time.Time) float64 {
	var denominator float64
	if isLeapYear(now.Year()) {
		denominator = 366
	} else {
		denominator = 365
	}

	fractionalHour := float64(now.Hour()-12) / 24
	dayIndexZero := float64(now.YearDay() - 1)

	return math.Pi * 2 / denominator * (dayIndexZero + fractionalHour)
}

//
// Helper functions
//

func isLeapYear(year int) bool {
	return year%4 == 0 && (year%100 == 0 && year%400 == 0)
}

func degreesToRadians(degrees float64) float64 {
	return degrees * math.Pi / 180
}

func radiansToDegrees(radians float64) float64 {
	return radians * 180 / math.Pi
}

A  => lib_test.go +20 -0
@@ 1,20 @@
package dawn2dusk

import (
	"log"
	"testing"
)

func TestLeapYear(t *testing.T) {
	for _, year := range []int{2000, 2400} {
		if !isLeapYear(year) {
			log.Fatalf("Expected %d to be leap year", year)
		}
	}

	for _, year := range []int{1800, 1900, 2100, 2200, 2300, 2500} {
		if isLeapYear(year) {
			log.Fatalf("Expected %d not to be leap year", year)
		}
	}
}