Compare commits

...

1 Commits

Author SHA1 Message Date
Cursor Agent 8c33c0e5cd fix(cli): add fractional value support to extendedParseDuration
This change allows users to specify fractional values in duration strings,
such as '1.5d' for 1.5 days (36 hours) or '0.5h' for 30 minutes.

Previously, only integer values were supported for duration units. This was
documented as a FIXME in the codebase referencing PR #15040.

The implementation:
- Updates the regex to capture decimal numbers (e.g., '1.5', '0.25')
- Uses ParseFloat instead of ParseInt for number parsing
- Calculates durations using float64 multiplication before converting to
  time.Duration
- Adds comprehensive tests for various fractional value scenarios

Co-authored-by: kyle <kyle@carberry.com>
2026-02-02 16:43:40 +00:00
2 changed files with 43 additions and 27 deletions
+31 -27
View File
@@ -188,9 +188,9 @@ func isDigit(s string) bool {
// - d (days, interpreted as 24h)
// - y (years, interpreted as 8_760h)
//
// FIXME: handle fractional values as discussed in https://github.com/coder/coder/pull/15040#discussion_r1799261736
// Fractional values are supported for all units (e.g., "1.5d" for 36 hours).
func extendedParseDuration(raw string) (time.Duration, error) {
var d int64
var d float64
isPositive := true
// handle negative durations by checking for a leading '-'
@@ -203,48 +203,52 @@ func extendedParseDuration(raw string) (time.Duration, error) {
return 0, xerrors.Errorf("invalid duration: %q", raw)
}
// Regular expression to match any characters that do not match the expected duration format
invalidCharRe := regexp.MustCompile(`[^0-9|nsuµhdym]+`)
// Regular expression to match any characters that do not match the expected
// duration format. Allows digits, decimal point, and unit characters.
invalidCharRe := regexp.MustCompile(`[^0-9.|nsuµhdym]+`)
if invalidCharRe.MatchString(raw) {
return 0, xerrors.Errorf("invalid duration format: %q", raw)
}
// Regular expression to match numbers followed by 'd', 'y', or time units
re := regexp.MustCompile(`(-?\d+)(ns|us|µs|ms|s|m|h|d|y)`)
// Regular expression to match numbers (including decimals) followed by time
// units. Captures the numeric part (with optional decimal) and the unit.
re := regexp.MustCompile(`(\d+\.?\d*)(ns|us|µs|ms|s|m|h|d|y)`)
matches := re.FindAllStringSubmatch(raw, -1)
for _, match := range matches {
var num int64
num, err := strconv.ParseInt(match[1], 10, 0)
num, err := strconv.ParseFloat(match[1], 64)
if err != nil {
return 0, xerrors.Errorf("invalid duration: %q", match[1])
}
var add float64
switch match[2] {
case "d":
// we want to check if d + num * int64(24*time.Hour) would overflow
if d > (1<<63-1)-num*int64(24*time.Hour) {
return 0, xerrors.Errorf("invalid duration: %q", raw)
}
d += num * int64(24*time.Hour)
add = num * float64(24*time.Hour)
case "y":
// we want to check if d + num * int64(8760*time.Hour) would overflow
if d > (1<<63-1)-num*int64(8760*time.Hour) {
return 0, xerrors.Errorf("invalid duration: %q", raw)
}
d += num * int64(8760*time.Hour)
case "h", "m", "s", "ns", "us", "µs", "ms":
partDuration, err := time.ParseDuration(match[0])
if err != nil {
return 0, xerrors.Errorf("invalid duration: %q", match[0])
}
if d > (1<<63-1)-int64(partDuration) {
return 0, xerrors.Errorf("invalid duration: %q", raw)
}
d += int64(partDuration)
add = num * float64(8760*time.Hour)
case "h":
add = num * float64(time.Hour)
case "m":
add = num * float64(time.Minute)
case "s":
add = num * float64(time.Second)
case "ms":
add = num * float64(time.Millisecond)
case "us", "µs":
add = num * float64(time.Microsecond)
case "ns":
add = num * float64(time.Nanosecond)
default:
return 0, xerrors.Errorf("invalid duration unit: %q", match[2])
}
// Check for overflow before adding.
const maxDuration = float64(1<<63 - 1)
if d > maxDuration-add {
return 0, xerrors.Errorf("invalid duration: %q", raw)
}
d += add
}
if !isPositive {
+12
View File
@@ -69,6 +69,18 @@ func TestExtendedParseDuration(t *testing.T) {
{"92233754775807y", 0, false},
{"200y200y200y200y200y", 0, false},
{"9223372036854775807s", 0, false},
// fractional values
{"1.5d", 36 * time.Hour, true},
{"0.5h", 30 * time.Minute, true},
{"2.5s", 2500 * time.Millisecond, true},
{"1.5y", time.Duration(float64(365*24*time.Hour) * 1.5), true},
{"0.5m", 30 * time.Second, true},
{"1.5h30m", 2 * time.Hour, true},
{"0.25d", 6 * time.Hour, true},
{"-1.5h", -90 * time.Minute, true},
{"100.5ms", 100*time.Millisecond + 500*time.Microsecond, true},
{"1.5us", 1500 * time.Nanosecond, true},
{"1.5µs", 1500 * time.Nanosecond, true},
} {
t.Run(testCase.Duration, func(t *testing.T) {
t.Parallel()