Skip to content

internal/timeutil: report range resolution #5

Description

@Saber5656

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

  1. 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).
  2. LoadLocation(name string) (*time.Location, error)""time.Local;
    otherwise time.LoadLocation (errors propagate for config validation).
  3. 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".
  4. Today(now time.Time, loc *time.Location) Day.
  5. 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).
  6. PrevWorkday(d Day) Day — subtract one day; while result is Saturday or
    Sunday keep subtracting. PrevDay(d Day) Day — literal minus one.
  7. 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).
  8. All functions pure; now always injected (no time.Now() inside the
    package) so tests are deterministic.

Acceptance Criteria

  • Table tests cover: normal weekday; Monday→Friday lookback (with
    previousWorkday=true) and Monday→Sunday (false); Sunday and Saturday
    standup dates; year boundary (Jan 1); DST spring-forward and fall-back
    days in America/New_York (23h and 25h day lengths asserted via
    End.Sub(Start)); Asia/Tokyo no-DST sanity.
  • ParseDate rejects: 2026-7-01, 2026/07/01, 20260701,
    2026-13-01, 2026-02-30, empty string.
  • Range.Overlaps correct for: instant inside, instant at End
    (excluded), span straddling Start, span fully before/after.
  • No time.Now() call inside the package (grep asserted in test or
    review).
  • ≥ 95% statement coverage; stdlib-only imports.

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions