flake-update-20260201
1// Package timerange provides natural language time range parsing.
2package timerange
3
4import (
5 "fmt"
6 "regexp"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/vdemeester/home/tools/review-tool/internal/activity"
12)
13
14var (
15 pastRe = regexp.MustCompile(`^past\s+(\d+)\s+(day|days|week|weeks|month|months)$`)
16 lastNRe = regexp.MustCompile(`^last\s+(\d+)\s+(day|days|week|weeks|month|months)$`)
17 dateRangeRe = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$`)
18)
19
20// Parse converts natural language to a time range.
21func Parse(input string) (*activity.TimeRange, error) {
22 input = strings.ToLower(strings.TrimSpace(input))
23 now := time.Now()
24
25 switch input {
26 case "today":
27 start := startOfDay(now)
28 return &activity.TimeRange{Start: start, End: now, Description: "today"}, nil
29
30 case "yesterday":
31 yesterday := now.AddDate(0, 0, -1)
32 return &activity.TimeRange{
33 Start: startOfDay(yesterday),
34 End: endOfDay(yesterday),
35 Description: "yesterday",
36 }, nil
37
38 case "this week":
39 start := startOfWeek(now)
40 return &activity.TimeRange{Start: start, End: now, Description: "this week"}, nil
41
42 case "last week":
43 thisWeekStart := startOfWeek(now)
44 lastWeekStart := thisWeekStart.AddDate(0, 0, -7)
45 return &activity.TimeRange{
46 Start: lastWeekStart,
47 End: thisWeekStart.Add(-time.Second),
48 Description: "last week",
49 }, nil
50
51 case "this month":
52 start := startOfMonth(now)
53 return &activity.TimeRange{Start: start, End: now, Description: "this month"}, nil
54
55 case "last month":
56 thisMonthStart := startOfMonth(now)
57 lastMonthStart := thisMonthStart.AddDate(0, -1, 0)
58 return &activity.TimeRange{
59 Start: lastMonthStart,
60 End: thisMonthStart.Add(-time.Second),
61 Description: "last month",
62 }, nil
63 }
64
65 // Try "past N days/weeks/months"
66 if matches := pastRe.FindStringSubmatch(input); len(matches) == 3 {
67 return parseDuration(matches[1], matches[2], now, input)
68 }
69
70 // Try "last N days/weeks/months"
71 if matches := lastNRe.FindStringSubmatch(input); len(matches) == 3 {
72 return parseDuration(matches[1], matches[2], now, input)
73 }
74
75 // Try date range "2026-01-01..2026-01-15"
76 if matches := dateRangeRe.FindStringSubmatch(input); len(matches) == 3 {
77 return parseDateRange(matches[1], matches[2])
78 }
79
80 return nil, fmt.Errorf("unrecognized time range: %q", input)
81}
82
83func parseDuration(nStr, unit string, now time.Time, desc string) (*activity.TimeRange, error) {
84 n, err := strconv.Atoi(nStr)
85 if err != nil {
86 return nil, fmt.Errorf("invalid number: %s", nStr)
87 }
88
89 var start time.Time
90 switch {
91 case strings.HasPrefix(unit, "day"):
92 start = now.AddDate(0, 0, -n)
93 case strings.HasPrefix(unit, "week"):
94 start = now.AddDate(0, 0, -7*n)
95 case strings.HasPrefix(unit, "month"):
96 start = now.AddDate(0, -n, 0)
97 default:
98 return nil, fmt.Errorf("unknown unit: %s", unit)
99 }
100
101 return &activity.TimeRange{
102 Start: startOfDay(start),
103 End: now,
104 Description: desc,
105 }, nil
106}
107
108func parseDateRange(startStr, endStr string) (*activity.TimeRange, error) {
109 start, err := time.Parse("2006-01-02", startStr)
110 if err != nil {
111 return nil, fmt.Errorf("invalid start date: %w", err)
112 }
113
114 end, err := time.Parse("2006-01-02", endStr)
115 if err != nil {
116 return nil, fmt.Errorf("invalid end date: %w", err)
117 }
118
119 return &activity.TimeRange{
120 Start: startOfDay(start),
121 End: endOfDay(end),
122 Description: fmt.Sprintf("%s to %s", startStr, endStr),
123 }, nil
124}
125
126func startOfDay(t time.Time) time.Time {
127 return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
128}
129
130func endOfDay(t time.Time) time.Time {
131 return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
132}
133
134func startOfWeek(t time.Time) time.Time {
135 weekday := int(t.Weekday())
136 if weekday == 0 {
137 weekday = 7 // Sunday is 7, not 0
138 }
139 return startOfDay(t.AddDate(0, 0, -weekday+1)) // Monday
140}
141
142func startOfMonth(t time.Time) time.Time {
143 return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
144}