Skip to content

Commit 618b81b

Browse files
committed
crhumanize: add Duration
1 parent 7ff5051 commit 618b81b

File tree

3 files changed

+213
-0
lines changed

3 files changed

+213
-0
lines changed

crhumanize/duration.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
11+
// implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package crhumanize
15+
16+
import (
17+
"fmt"
18+
"time"
19+
)
20+
21+
// Duration returns a simplified approximation (±5%) of a duration.
22+
//
23+
// Examples:
24+
// - 123.456µs -> "123µs"
25+
// - 1.234567ms -> "1.2ms"
26+
// - 59.1s -> "59s"
27+
// - 1m6.5s -> 1m7s
28+
func Duration(d time.Duration) SafeString {
29+
if d < 0 {
30+
return "-" + Duration(-d)
31+
}
32+
if d == 0 {
33+
return "0s"
34+
}
35+
36+
if d < 10*time.Minute {
37+
// Round to a precision that gives us one decimal when the value is a single
38+
// digit (e.g. "1.5ms" vs "12ms")
39+
r := time.Nanosecond
40+
switch {
41+
case d < time.Microsecond:
42+
case d < 10*time.Microsecond:
43+
r = 100 * time.Nanosecond
44+
case d < time.Millisecond:
45+
r = time.Microsecond
46+
case d < 10*time.Millisecond:
47+
r = 100 * time.Microsecond
48+
case d < time.Second:
49+
r = time.Millisecond
50+
case d < 10*time.Second:
51+
r = 100 * time.Millisecond
52+
default:
53+
r = time.Second
54+
}
55+
return SafeString(d.Round(r).String())
56+
}
57+
58+
if d > 100*time.Hour {
59+
d = d.Round(time.Hour)
60+
} else {
61+
d = d.Round(time.Minute)
62+
}
63+
h := int(d / time.Hour)
64+
m := int((d % time.Hour) / time.Minute)
65+
switch {
66+
case h == 0:
67+
return SafeString(fmt.Sprintf("%dm", m))
68+
case m == 0:
69+
return SafeString(fmt.Sprintf("%dh", h))
70+
default:
71+
return SafeString(fmt.Sprintf("%dh%dm", h, m))
72+
}
73+
}

crhumanize/duration_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
// implied. See the License for the specific language governing
13+
// permissions and limitations under the License.
14+
15+
package crhumanize
16+
17+
import (
18+
"fmt"
19+
"math"
20+
"math/rand/v2"
21+
"strings"
22+
"testing"
23+
"time"
24+
25+
"github.com/cockroachdb/crlib/crstrings"
26+
"github.com/cockroachdb/crlib/internal/datadriven"
27+
)
28+
29+
func TestDuration(t *testing.T) {
30+
datadriven.RunTest(t, "testdata/duration", func(t *testing.T, td *datadriven.TestData) string {
31+
if td.Cmd != "duration" {
32+
td.Fatalf(t, "unknown command: %q", td.Cmd)
33+
}
34+
var buf strings.Builder
35+
for _, l := range crstrings.Lines(td.Input) {
36+
d, err := time.ParseDuration(l)
37+
if err != nil {
38+
td.Fatalf(t, "could not parse duration %q: %v", l, err)
39+
}
40+
fmt.Fprintf(&buf, "%s -> %s\n", d, Duration(d))
41+
}
42+
return buf.String()
43+
})
44+
}
45+
46+
func TestDurationError(t *testing.T) {
47+
for _, v := range []time.Duration{time.Microsecond, time.Second, time.Minute, time.Hour, 100 * time.Hour, 10000 * time.Hour} {
48+
for i := 0; i < 1000; i++ {
49+
d := time.Duration(rand.Int64N(int64(v)))
50+
s := string(Duration(d))
51+
d1, err := time.ParseDuration(s)
52+
if err != nil {
53+
t.Fatalf("%s: could not parse duration %q: %v", d, s, err)
54+
}
55+
if relativeErr := math.Abs(float64(d1-d)) / float64(d); relativeErr > 0.05 {
56+
t.Fatalf("%s -> %s -> %s error is too large: %f\n", d, s, d1, relativeErr)
57+
}
58+
}
59+
}
60+
}

crhumanize/testdata/duration

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
duration
2+
1ns
3+
12345ns
4+
123456ns
5+
1234567ns
6+
5ms
7+
500ms
8+
900ms
9+
1s
10+
1001ms
11+
1011ms
12+
1111ms
13+
1500ms
14+
----
15+
1ns -> 1ns
16+
12.345µs -> 12µs
17+
123.456µs -> 123µs
18+
1.234567ms -> 1.2ms
19+
5ms -> 5ms
20+
500ms -> 500ms
21+
900ms -> 900ms
22+
1s -> 1s
23+
1.001s -> 1s
24+
1.011s -> 1s
25+
1.111s -> 1.1s
26+
1.5s -> 1.5s
27+
28+
29+
duration
30+
15s
31+
30s
32+
31s
33+
45s
34+
59.1s
35+
60s
36+
61s
37+
66.5s
38+
90.2s
39+
119s
40+
121s
41+
40m20s
42+
----
43+
15s -> 15s
44+
30s -> 30s
45+
31s -> 31s
46+
45s -> 45s
47+
59.1s -> 59s
48+
1m0s -> 1m0s
49+
1m1s -> 1m1s
50+
1m6.5s -> 1m7s
51+
1m30.2s -> 1m30s
52+
1m59s -> 1m59s
53+
2m1s -> 2m1s
54+
40m20s -> 40m
55+
56+
duration
57+
1h1m15s
58+
1h31m13s
59+
2h3m
60+
20h34m50s
61+
205h45m15s
62+
2057h36m
63+
20576h7m
64+
205761h18m
65+
2057613h9m
66+
----
67+
1h1m15s -> 1h1m
68+
1h31m13s -> 1h31m
69+
2h3m0s -> 2h3m
70+
20h34m50s -> 20h35m
71+
205h45m15s -> 206h
72+
2057h36m0s -> 2058h
73+
20576h7m0s -> 20576h
74+
205761h18m0s -> 205761h
75+
2057613h9m0s -> 2057613h
76+
77+
duration
78+
1h15m13s
79+
----
80+
1h15m13s -> 1h15m

0 commit comments

Comments
 (0)