@@ -0,0 +1,46 @@
|
||||
// Package daterange resolves recurring calendar windows and normalized dates.
|
||||
package daterange
|
||||
|
||||
import "time"
|
||||
|
||||
// CalendarWindow resolves a recurring month/day range around now. A range whose
|
||||
// start month is after its end month crosses the year boundary.
|
||||
func CalendarWindow(now time.Time, monthFrom int, dayFrom *int, monthTo int, dayTo *int) (time.Time, time.Time) {
|
||||
location := now.Location()
|
||||
startYear := now.Year()
|
||||
if monthFrom > monthTo && int(now.Month()) <= monthTo {
|
||||
startYear--
|
||||
}
|
||||
endYear := startYear
|
||||
if monthFrom > monthTo {
|
||||
endYear++
|
||||
}
|
||||
startDay := 1
|
||||
if dayFrom != nil {
|
||||
startDay = clampDay(startYear, time.Month(monthFrom), *dayFrom)
|
||||
}
|
||||
endDay := daysInMonth(endYear, time.Month(monthTo))
|
||||
if dayTo != nil {
|
||||
endDay = clampDay(endYear, time.Month(monthTo), *dayTo)
|
||||
}
|
||||
return time.Date(startYear, time.Month(monthFrom), startDay, 0, 0, 0, 0, location), time.Date(endYear, time.Month(monthTo), endDay, 23, 59, 59, 0, location)
|
||||
}
|
||||
|
||||
// Date returns value at midnight in its original location.
|
||||
func Date(value time.Time) time.Time {
|
||||
return time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 0, 0, value.Location())
|
||||
}
|
||||
|
||||
func daysInMonth(year int, month time.Month) int {
|
||||
return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
|
||||
}
|
||||
|
||||
func clampDay(year int, month time.Month, day int) int {
|
||||
if maximum := daysInMonth(year, month); day > maximum {
|
||||
return maximum
|
||||
}
|
||||
if day < 1 {
|
||||
return 1
|
||||
}
|
||||
return day
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package daterange
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func intPointer(value int) *int { return &value }
|
||||
|
||||
func TestCalendarWindow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
now time.Time
|
||||
from int
|
||||
fromDay *int
|
||||
to int
|
||||
toDay *int
|
||||
wantStart, wantEnd string
|
||||
}{
|
||||
{"ordinary", time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), 2, nil, 3, nil, "2026-02-01", "2026-03-31"},
|
||||
{"wrap before end", time.Date(2026, 1, 10, 0, 0, 0, 0, time.UTC), 11, nil, 2, nil, "2025-11-01", "2026-02-28"},
|
||||
{"wrap before start", time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC), 11, nil, 2, nil, "2026-11-01", "2027-02-28"},
|
||||
{"clamps invalid month day", time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), 2, intPointer(31), 2, intPointer(31), "2026-02-28", "2026-02-28"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
start, end := CalendarWindow(test.now, test.from, test.fromDay, test.to, test.toDay)
|
||||
if got := start.Format("2006-01-02"); got != test.wantStart {
|
||||
t.Errorf("start=%s", got)
|
||||
}
|
||||
if got := end.Format("2006-01-02"); got != test.wantEnd {
|
||||
t.Errorf("end=%s", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user