Skip to content

Commit 2ac5723

Browse files
authored
fix(duration): accepts fractional values for duration (#170)
* fix(duration): accepts fractional values for duration * fixes #169 Signed-off-by: Frederic BIDON <[email protected]> * one more test case Signed-off-by: Frederic BIDON <[email protected]> --------- Signed-off-by: Frederic BIDON <[email protected]>
1 parent aae2676 commit 2ac5723

File tree

2 files changed

+84
-2
lines changed

2 files changed

+84
-2
lines changed

duration.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"database/sql/driver"
1919
"encoding/json"
2020
"fmt"
21+
"math"
2122
"regexp"
2223
"strconv"
2324
"strings"
@@ -60,7 +61,7 @@ var (
6061
"w": hoursInDay * daysInWeek * time.Hour,
6162
}
6263

63-
durationMatcher = regexp.MustCompile(`(((?:-\s?)?\d+)\s*([A-Za-zµ]+))`)
64+
durationMatcher = regexp.MustCompile(`^(((?:-\s?)?\d+)(\.\d+)?\s*([A-Za-zµ]+))`)
6465
)
6566

6667
// IsDuration returns true if the provided string is a valid duration
@@ -100,11 +101,24 @@ func ParseDuration(cand string) (time.Duration, error) {
100101

101102
var dur time.Duration
102103
ok := false
104+
const expectGroups = 4
103105
for _, match := range durationMatcher.FindAllStringSubmatch(cand, -1) {
106+
if len(match) < expectGroups {
107+
continue
108+
}
104109

105110
// remove possible leading - and spaces
106111
value, negative := strings.CutPrefix(match[2], "-")
107112

113+
// if the duration contains a decimal separator determine a divising factor
114+
const neutral = 1.0
115+
divisor := neutral
116+
decimal, hasDecimal := strings.CutPrefix(match[3], ".")
117+
if hasDecimal {
118+
divisor = math.Pow10(len(decimal))
119+
value += decimal // consider the value as an integer: will change units later on
120+
}
121+
108122
// if the string is a valid duration, parse it
109123
factor, err := strconv.Atoi(strings.TrimSpace(value)) // converts string to int
110124
if err != nil {
@@ -115,7 +129,7 @@ func ParseDuration(cand string) (time.Duration, error) {
115129
factor = -factor
116130
}
117131

118-
unit := strings.ToLower(strings.TrimSpace(match[3]))
132+
unit := strings.ToLower(strings.TrimSpace(match[4]))
119133

120134
for _, variants := range timeUnits {
121135
last := len(variants) - 1
@@ -124,6 +138,9 @@ func ParseDuration(cand string) (time.Duration, error) {
124138
for i, variant := range variants {
125139
if (last == i && strings.HasPrefix(unit, variant)) || strings.EqualFold(variant, unit) {
126140
ok = true
141+
if divisor != neutral {
142+
multiplier = time.Duration(float64(multiplier) / divisor) // convert to duration only after having reduced the scale
143+
}
127144
dur += (time.Duration(factor) * multiplier)
128145
}
129146
}

duration_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package strfmt
1616

1717
import (
18+
"fmt"
1819
"testing"
1920
"time"
2021

@@ -230,3 +231,67 @@ func TestDeepCopyDuration(t *testing.T) {
230231
out3 := inNil.DeepCopy()
231232
assert.Nil(t, out3)
232233
}
234+
235+
func TestIssue169FractionalDuration(t *testing.T) {
236+
for _, tt := range []struct {
237+
Input string
238+
Expected string
239+
ExpectError bool
240+
}{
241+
{
242+
Input: "1.5 h",
243+
Expected: "1h30m0s",
244+
},
245+
{
246+
Input: "1.5 d",
247+
Expected: "36h0m0s",
248+
},
249+
{
250+
Input: "3.14159 d",
251+
Expected: "75h23m53.376s",
252+
},
253+
{
254+
Input: "- 3.14159 d",
255+
Expected: "-75h23m53.376s",
256+
},
257+
{
258+
Input: "3.141.59 d",
259+
ExpectError: true,
260+
},
261+
{
262+
Input: ".314159 d",
263+
ExpectError: true,
264+
},
265+
{
266+
Input: "314159. d",
267+
ExpectError: true,
268+
},
269+
} {
270+
fractionalDuration := tt
271+
272+
if fractionalDuration.ExpectError {
273+
t.Run(fmt.Sprintf("invalid fractional duration %s should NOT parse", fractionalDuration.Input), func(t *testing.T) {
274+
t.Parallel()
275+
276+
require.False(t, IsDuration(fractionalDuration.Input))
277+
})
278+
279+
continue
280+
}
281+
282+
t.Run(fmt.Sprintf("fractional duration %s should parse", fractionalDuration.Input), func(t *testing.T) {
283+
t.Parallel()
284+
285+
require.True(t, IsDuration(fractionalDuration.Input))
286+
287+
var d Duration
288+
require.NoError(t, d.UnmarshalText([]byte(fractionalDuration.Input)))
289+
290+
require.Equal(t, fractionalDuration.Expected, d.String())
291+
292+
dd, err := ParseDuration(fractionalDuration.Input)
293+
require.NoError(t, err)
294+
require.Equal(t, fractionalDuration.Expected, dd.String())
295+
})
296+
}
297+
}

0 commit comments

Comments
 (0)