Add configuration and CLI foundation

This commit is contained in:
2026-05-29 16:58:18 +00:00
parent e5cd23de48
commit 8c065751c2
19 changed files with 1068 additions and 43 deletions

View File

@@ -0,0 +1,22 @@
// Package timeutil provides time-zone, clock, and period helpers.
package timeutil
import "time"
type Clock interface {
Now() time.Time
}
type SystemClock struct{}
func (SystemClock) Now() time.Time {
return time.Now()
}
type FixedClock struct {
Time time.Time
}
func (c FixedClock) Now() time.Time {
return c.Time
}

View File

@@ -0,0 +1,49 @@
package timeutil
import (
"fmt"
"strings"
"time"
)
const DateLayout = "2006-01-02"
const LocalDateTimeLayout = "2006-01-02T15:04"
func LoadLocation(name string) (*time.Location, error) {
location, err := time.LoadLocation(name)
if err == nil {
return location, nil
}
if strings.Contains(name, "/") {
return nil, fmt.Errorf("load timezone %q: %w", name, err)
}
location, chicagoErr := time.LoadLocation("America/" + name)
if chicagoErr == nil {
return location, nil
}
return nil, fmt.Errorf("load timezone %q: %w", name, err)
}
func LocalDate(now time.Time, location *time.Location) time.Time {
local := now.In(location)
return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, location)
}
func ParseLocalDate(value string, location *time.Location) (time.Time, error) {
parsed, err := time.ParseInLocation(DateLayout, value, location)
if err != nil {
return time.Time{}, fmt.Errorf("parse date %q as YYYY-MM-DD: %w", value, err)
}
return parsed, nil
}
func ParseStormTime(value string, location *time.Location) (time.Time, error) {
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
return parsed, nil
}
parsed, err := time.ParseInLocation(LocalDateTimeLayout, value, location)
if err != nil {
return time.Time{}, fmt.Errorf("parse storm time %q as YYYY-MM-DDTHH:MM or RFC3339: %w", value, err)
}
return parsed, nil
}

View File

@@ -0,0 +1,49 @@
package timeutil
import (
"testing"
"time"
)
func TestLoadLocationAcceptsChicagoAlias(t *testing.T) {
location, err := LoadLocation("Chicago")
if err != nil {
t.Fatalf("LoadLocation() error = %v", err)
}
if location.String() != "America/Chicago" {
t.Fatalf("location = %q, want America/Chicago", location.String())
}
}
func TestParseLocalDate(t *testing.T) {
location := time.FixedZone("Test", -5*60*60)
got, err := ParseLocalDate("2026-05-29", location)
if err != nil {
t.Fatalf("ParseLocalDate() error = %v", err)
}
if got.Format(DateLayout) != "2026-05-29" {
t.Fatalf("date = %s, want 2026-05-29", got.Format(DateLayout))
}
if got.Location() != location {
t.Fatalf("location = %v, want test location", got.Location())
}
}
func TestParseStormTime(t *testing.T) {
location := time.FixedZone("Test", -5*60*60)
local, err := ParseStormTime("2026-05-29T18:00", location)
if err != nil {
t.Fatalf("ParseStormTime(local) error = %v", err)
}
if local.Location() != location {
t.Fatalf("local location = %v, want test location", local.Location())
}
rfc3339, err := ParseStormTime("2026-05-29T18:00:00-05:00", location)
if err != nil {
t.Fatalf("ParseStormTime(rfc3339) error = %v", err)
}
if rfc3339.Format(time.RFC3339) != "2026-05-29T18:00:00-05:00" {
t.Fatalf("rfc3339 = %s, want preserved offset time", rfc3339.Format(time.RFC3339))
}
}