Title
internal/timeutil: report range resolution
Summary
Implement the date/time helpers that turn CLI inputs (--date, configured
timezone) into concrete half-open time ranges, including the
previous-workday logic used by standup (DESIGN.md §9.1–§9.2).
Context
Every provider filters events by a timeutil.Range; daily needs "that day
in the report timezone"; standup needs a yesterday bucket (previous
workday) and a today bucket (midnight → now). Getting timezone and DST
boundaries right here prevents a whole class of off-by-one-day bugs.
Scope
internal/timeutil/timeutil.go + tests. Stdlib only.
Detailed Requirements
type Range struct { Start, End time.Time } — half-open [Start, End),
with Contains(t time.Time) bool and Overlaps(start, end time.Time) bool (for span events: end may be zero ⇒ treat as instantaneous).
LoadLocation(name string) (*time.Location, error) — "" → time.Local;
otherwise time.LoadLocation (errors propagate for config validation).
ParseDate(s string, loc *time.Location) (Day, error) — strict
YYYY-MM-DD (time.ParseInLocation("2006-01-02"); reject anything else,
including 2026-7-1). type Day struct { Y int; M time.Month; D int; Loc *time.Location } with String() "2026-07-10".
Today(now time.Time, loc *time.Location) Day.
DayRange(d Day) Range — midnight to next midnight via
time.Date(...).AddDate(0,0,1) (DST-safe: a 23h/25h day is whatever the
location says, never a fixed 24h addition).
PrevWorkday(d Day) Day — subtract one day; while result is Saturday or
Sunday keep subtracting. PrevDay(d Day) Day — literal minus one.
StandupBuckets(d Day, now time.Time, previousWorkday bool) (yesterday Range, today Range, yesterdayDay Day):
yesterday = full DayRange of PrevWorkday(d) (or PrevDay when
previousWorkday == false).
today = [midnight(d), now) when d is the same calendar day as
now in d.Loc; otherwise full DayRange(d) (retro-generation, per
DESIGN §9.2).
- All functions pure;
now always injected (no time.Now() inside the
package) so tests are deterministic.
Acceptance Criteria
Validation
go test -race -cover ./internal/timeutil/ pasted into PR. Reviewer spot
check: TZ=America/New_York go test ./internal/timeutil/ also green
(host-TZ independence).
Dependencies
Non-goals
Holiday calendars (v1 non-goal), week/month ranges (v2 rollups), parsing
times from source files (each parser owns its own timestamp parsing).
Design References
docs/DESIGN.md §4 (--date), §9.1, §9.2
Source of truth: docs/issues/04-timeutil-package.md (PR #1, branch docs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies: docs/ISSUE_PLAN.md (this is issue 04 of 33).
Title
internal/timeutil: report range resolution
Summary
Implement the date/time helpers that turn CLI inputs (
--date, configuredtimezone) into concrete half-open time ranges, including the
previous-workday logic used by standup (DESIGN.md §9.1–§9.2).
Context
Every provider filters events by a
timeutil.Range;dailyneeds "that dayin the report timezone";
standupneeds a yesterday bucket (previousworkday) and a today bucket (midnight → now). Getting timezone and DST
boundaries right here prevents a whole class of off-by-one-day bugs.
Scope
internal/timeutil/timeutil.go+ tests. Stdlib only.Detailed Requirements
type Range struct { Start, End time.Time }— half-open[Start, End),with
Contains(t time.Time) boolandOverlaps(start, end time.Time) bool(for span events:endmay be zero ⇒ treat as instantaneous).LoadLocation(name string) (*time.Location, error)—""→time.Local;otherwise
time.LoadLocation(errors propagate for config validation).ParseDate(s string, loc *time.Location) (Day, error)— strictYYYY-MM-DD(time.ParseInLocation("2006-01-02"); reject anything else,including
2026-7-1).type Day struct { Y int; M time.Month; D int; Loc *time.Location }withString() "2026-07-10".Today(now time.Time, loc *time.Location) Day.DayRange(d Day) Range— midnight to next midnight viatime.Date(...).AddDate(0,0,1)(DST-safe: a 23h/25h day is whatever thelocation says, never a fixed 24h addition).
PrevWorkday(d Day) Day— subtract one day; while result is Saturday orSunday keep subtracting.
PrevDay(d Day) Day— literal minus one.StandupBuckets(d Day, now time.Time, previousWorkday bool) (yesterday Range, today Range, yesterdayDay Day):yesterday= fullDayRangeofPrevWorkday(d)(orPrevDaywhenpreviousWorkday == false).today=[midnight(d), now)whendis the same calendar day asnowind.Loc; otherwise fullDayRange(d)(retro-generation, perDESIGN §9.2).
nowalways injected (notime.Now()inside thepackage) so tests are deterministic.
Acceptance Criteria
previousWorkday=true) and Monday→Sunday (false); Sunday and Saturdaystandup dates; year boundary (Jan 1); DST spring-forward and fall-back
days in
America/New_York(23h and 25h day lengths asserted viaEnd.Sub(Start));Asia/Tokyono-DST sanity.ParseDaterejects:2026-7-01,2026/07/01,20260701,2026-13-01,2026-02-30, empty string.Range.Overlapscorrect for: instant inside, instant at End(excluded), span straddling Start, span fully before/after.
time.Now()call inside the package (grep asserted in test orreview).
Validation
go test -race -cover ./internal/timeutil/pasted into PR. Reviewer spotcheck:
TZ=America/New_York go test ./internal/timeutil/also green(host-TZ independence).
Dependencies
Non-goals
Holiday calendars (v1 non-goal), week/month ranges (v2 rollups), parsing
times from source files (each parser owns its own timestamp parsing).
Design References
docs/DESIGN.md§4 (--date), §9.1, §9.2Source of truth:
docs/issues/04-timeutil-package.md(PR #1, branchdocs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies:docs/ISSUE_PLAN.md(this is issue 04 of 33).