Skip to content

Commit bb5731c

Browse files
feat(time): add flexible time format support with case-insensitive parsing
Enhanced time parsing to support natural language variations and case-insensitive formats for improved user experience. New Supported Formats: - Long forms: 5min, 5minutes, 2hr, 2hours, 3days, 1week - Plural variations: mins, hrs, days, weeks - With spaces: "5 minutes", "2 hours" - Minus prefix: -5m, -2h, -10minutes (gracefully handled) - Case-insensitive: NOW, 5MIN, 2HOURS, etc. Changes: - pkg/util/time.go: Enhanced regex to match long forms and spaces - pkg/util/time.go: Added case-insensitive matching for all formats - pkg/util/time_test.go: Added comprehensive tests for all new variations - cmd/logs_simple.go: Updated help text to document new formats All existing functionality preserved, fully backward compatible. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent e1934ea commit bb5731c

3 files changed

Lines changed: 140 additions & 18 deletions

File tree

cmd/logs_simple.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ LOG QUERY SYNTAX:
5454
5555
TIME RANGES:
5656
Supported time formats:
57-
• Relative: 1h, 30m, 7d, 1w (hour, minute, day, week)
57+
• Relative short: 1h, 30m, 7d, 5s, 1w
58+
• Relative long: 5min, 5minutes, 2hr, 2hours, 3days, 1week
59+
• With spaces: "5 minutes", "2 hours"
60+
• With minus: -5m, -2h (treated same as 5m, 2h)
5861
• Absolute: Unix timestamp in milliseconds
62+
• RFC3339: 2024-01-01T00:00:00Z
5963
• now: Current time
6064
6165
EXAMPLES:
@@ -557,8 +561,8 @@ var (
557561
func init() {
558562
// Search command flags (v1)
559563
logsSearchCmd.Flags().StringVar(&logsQuery, "query", "", "Search query (required)")
560-
logsSearchCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 30m, 7d, RFC3339, or Unix timestamp")
561-
logsSearchCmd.Flags().StringVar(&logsTo, "to", "now", "End time: 1h, 30m, now, RFC3339, or Unix timestamp")
564+
logsSearchCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'")
565+
logsSearchCmd.Flags().StringVar(&logsTo, "to", "now", "End time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'")
562566
logsSearchCmd.Flags().IntVar(&logsLimit, "limit", 50, "Maximum number of logs (1-1000)")
563567
logsSearchCmd.Flags().StringVar(&logsSort, "sort", "desc", "Sort order: asc or desc")
564568
logsSearchCmd.Flags().StringVar(&logsIndex, "index", "", "Comma-separated log indexes")
@@ -569,15 +573,15 @@ func init() {
569573

570574
// List command flags (v2)
571575
logsListCmd.Flags().StringVar(&logsQuery, "query", "*", "Search query")
572-
logsListCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 30m, 7d, RFC3339, or Unix timestamp")
576+
logsListCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'")
573577
logsListCmd.Flags().StringVar(&logsTo, "to", "now", "End time")
574578
logsListCmd.Flags().IntVar(&logsLimit, "limit", 10, "Number of logs")
575579
logsListCmd.Flags().StringVar(&logsSort, "sort", "-timestamp", "Sort order")
576580
logsListCmd.Flags().StringVar(&logsStorage, "storage", "", "Storage tier: indexes, online-archives, or flex")
577581

578582
// Query command flags (v2)
579583
logsQueryCmd.Flags().StringVar(&logsQuery, "query", "", "Log query (required)")
580-
logsQueryCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 30m, 7d, RFC3339, or Unix timestamp")
584+
logsQueryCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'")
581585
logsQueryCmd.Flags().StringVar(&logsTo, "to", "now", "End time")
582586
logsQueryCmd.Flags().IntVar(&logsLimit, "limit", 50, "Maximum results")
583587
logsQueryCmd.Flags().StringVar(&logsSort, "sort", "-timestamp", "Sort order")
@@ -589,7 +593,7 @@ func init() {
589593

590594
// Aggregate command flags (v2)
591595
logsAggregateCmd.Flags().StringVar(&logsQuery, "query", "", "Log query (required)")
592-
logsAggregateCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 30m, 7d, RFC3339, or Unix timestamp")
596+
logsAggregateCmd.Flags().StringVar(&logsFrom, "from", "1h", "Start time: 1h, 5min, 2hours, '5 minutes', RFC3339, Unix timestamp, or 'now'")
593597
logsAggregateCmd.Flags().StringVar(&logsTo, "to", "now", "End time")
594598
logsAggregateCmd.Flags().StringVar(&logsCompute, "compute", "count", "Metric to compute")
595599
logsAggregateCmd.Flags().StringVar(&logsGroupBy, "group-by", "", "Field to group by")

pkg/util/time.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,25 @@ import (
99
"fmt"
1010
"regexp"
1111
"strconv"
12+
"strings"
1213
"time"
1314
)
1415

1516
// ParseTimeParam parses time parameters supporting multiple formats:
1617
// - "now" for current time
1718
// - Unix timestamps (e.g., "1704067200")
18-
// - Relative time (e.g., "1h", "30m", "7d")
19+
// - Relative time with flexible formats:
20+
// - Short: "1h", "30m", "7d", "5s", "1w"
21+
// - Long: "5min", "5mins", "5minute", "5minutes"
22+
// - Long: "2hr", "2hrs", "2hour", "2hours"
23+
// - Long: "3day", "3days"
24+
// - Long: "1week", "1weeks"
25+
// - With spaces: "5 minutes", "2 hours"
26+
// - With minus prefix: "-5m", "-2h" (treated same as "5m", "2h")
1927
// - ISO date strings (e.g., "2024-01-01T00:00:00Z")
2028
func ParseTimeParam(timeStr string) (time.Time, error) {
21-
// Handle "now"
22-
if timeStr == "now" {
29+
// Handle "now" (case-insensitive)
30+
if strings.ToLower(timeStr) == "now" {
2331
return time.Now(), nil
2432
}
2533

@@ -31,27 +39,32 @@ func ParseTimeParam(timeStr string) (time.Time, error) {
3139
}
3240
}
3341

34-
// Try parsing relative time (e.g., "1h", "30m", "2d")
35-
re := regexp.MustCompile(`^(\d+)([smhd])$`)
42+
// Try parsing relative time with flexible formats
43+
// Supports: 5m, 5min, 5mins, 5minute, 5minutes, 5 minutes, -5m, etc.
44+
// Case-insensitive to handle MIN, Hour, HOURS, etc.
45+
re := regexp.MustCompile(`(?i)^-?(\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|week|weeks)$`)
3646
matches := re.FindStringSubmatch(timeStr)
3747
if len(matches) == 3 {
3848
value, err := strconv.Atoi(matches[1])
3949
if err != nil {
4050
return time.Time{}, fmt.Errorf("invalid time value: %w", err)
4151
}
4252

43-
unit := matches[2]
53+
unit := strings.ToLower(matches[2])
4454
var duration time.Duration
4555

56+
// Map all variations to their base duration
4657
switch unit {
47-
case "s":
58+
case "s", "sec", "secs", "second", "seconds":
4859
duration = time.Duration(value) * time.Second
49-
case "m":
60+
case "m", "min", "mins", "minute", "minutes":
5061
duration = time.Duration(value) * time.Minute
51-
case "h":
62+
case "h", "hr", "hrs", "hour", "hours":
5263
duration = time.Duration(value) * time.Hour
53-
case "d":
64+
case "d", "day", "days":
5465
duration = time.Duration(value) * 24 * time.Hour
66+
case "w", "week", "weeks":
67+
duration = time.Duration(value) * 7 * 24 * time.Hour
5568
}
5669

5770
return time.Now().Add(-duration), nil

pkg/util/time_test.go

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,114 @@ func TestParseTimeParam(t *testing.T) {
9696
wantError: true,
9797
},
9898
{
99-
name: "negative value",
99+
name: "negative value (minus prefix)",
100100
input: "-5h",
101-
wantError: true,
101+
wantError: false,
102+
checkFunc: func(t time.Time) bool {
103+
expected := now.Add(-5 * time.Hour)
104+
diff := expected.Sub(t).Abs()
105+
return diff < time.Second
106+
},
107+
},
108+
{
109+
name: "long form: minutes",
110+
input: "5minutes",
111+
wantError: false,
112+
checkFunc: func(t time.Time) bool {
113+
expected := now.Add(-5 * time.Minute)
114+
diff := expected.Sub(t).Abs()
115+
return diff < time.Second
116+
},
117+
},
118+
{
119+
name: "long form: min",
120+
input: "10min",
121+
wantError: false,
122+
checkFunc: func(t time.Time) bool {
123+
expected := now.Add(-10 * time.Minute)
124+
diff := expected.Sub(t).Abs()
125+
return diff < time.Second
126+
},
127+
},
128+
{
129+
name: "long form: hours",
130+
input: "2hours",
131+
wantError: false,
132+
checkFunc: func(t time.Time) bool {
133+
expected := now.Add(-2 * time.Hour)
134+
diff := expected.Sub(t).Abs()
135+
return diff < time.Second
136+
},
137+
},
138+
{
139+
name: "long form: hr",
140+
input: "3hr",
141+
wantError: false,
142+
checkFunc: func(t time.Time) bool {
143+
expected := now.Add(-3 * time.Hour)
144+
diff := expected.Sub(t).Abs()
145+
return diff < time.Second
146+
},
147+
},
148+
{
149+
name: "long form: hrs",
150+
input: "4hrs",
151+
wantError: false,
152+
checkFunc: func(t time.Time) bool {
153+
expected := now.Add(-4 * time.Hour)
154+
diff := expected.Sub(t).Abs()
155+
return diff < time.Second
156+
},
157+
},
158+
{
159+
name: "long form: days",
160+
input: "14days",
161+
wantError: false,
162+
checkFunc: func(t time.Time) bool {
163+
expected := now.Add(-14 * 24 * time.Hour)
164+
diff := expected.Sub(t).Abs()
165+
return diff < time.Second
166+
},
167+
},
168+
{
169+
name: "long form: weeks",
170+
input: "2weeks",
171+
wantError: false,
172+
checkFunc: func(t time.Time) bool {
173+
expected := now.Add(-2 * 7 * 24 * time.Hour)
174+
diff := expected.Sub(t).Abs()
175+
return diff < time.Second
176+
},
177+
},
178+
{
179+
name: "with space: minutes",
180+
input: "5 minutes",
181+
wantError: false,
182+
checkFunc: func(t time.Time) bool {
183+
expected := now.Add(-5 * time.Minute)
184+
diff := expected.Sub(t).Abs()
185+
return diff < time.Second
186+
},
187+
},
188+
{
189+
name: "with space: hours",
190+
input: "2 hours",
191+
wantError: false,
192+
checkFunc: func(t time.Time) bool {
193+
expected := now.Add(-2 * time.Hour)
194+
diff := expected.Sub(t).Abs()
195+
return diff < time.Second
196+
},
197+
},
198+
{
199+
name: "with minus prefix and long form",
200+
input: "-10minutes",
201+
wantError: false,
202+
checkFunc: func(t time.Time) bool {
203+
expected := now.Add(-10 * time.Minute)
204+
diff := expected.Sub(t).Abs()
205+
return diff < time.Second
206+
},
102207
},
103208
{
104209
name: "empty string",

0 commit comments

Comments
 (0)