Skip to content

Commit 1d6b7eb

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

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

crhumanize/duration.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 "time"
17+
18+
// Duration returns a simplified approximation (±10%) of a duration, using a
19+
// single unit and showing at most one decimal.
20+
//
21+
// Examples: "500ms", "0.8s", "23.1m", "1.5h".
22+
func Duration(d time.Duration) SafeString {
23+
// For very large durations, round to the nearest hour.
24+
if d > 100*time.Hour {
25+
d = d.Round(time.Hour)
26+
}
27+
unit, denom := func() (string, time.Duration) {
28+
switch {
29+
case d < 500*time.Nanosecond:
30+
return "ns", time.Nanosecond
31+
case d < 500*time.Microsecond:
32+
return "µs", time.Microsecond
33+
case d < 500*time.Millisecond:
34+
return "ms", time.Millisecond
35+
case d < time.Minute:
36+
return "s", time.Second
37+
case d < time.Hour:
38+
return "m", time.Minute
39+
default:
40+
return "h", time.Hour
41+
}
42+
}()
43+
44+
return Float(float64(d)/float64(denom), 1) + SafeString(unit)
45+
}

crhumanize/duration_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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/rand/v2"
20+
"strings"
21+
"testing"
22+
"time"
23+
24+
"github.com/cockroachdb/crlib/crstrings"
25+
"github.com/cockroachdb/crlib/internal/datadriven"
26+
)
27+
28+
func TestDuration(t *testing.T) {
29+
datadriven.RunTest(t, "testdata/duration", func(t *testing.T, td *datadriven.TestData) string {
30+
if td.Cmd != "duration" {
31+
td.Fatalf(t, "unknown command: %q", td.Cmd)
32+
}
33+
var buf strings.Builder
34+
for _, l := range crstrings.Lines(td.Input) {
35+
d, err := time.ParseDuration(l)
36+
if err != nil {
37+
td.Fatalf(t, "could not parse duration %q: %v", l, err)
38+
}
39+
fmt.Fprintf(&buf, "%s -> %s\n", d, Duration(d))
40+
}
41+
return buf.String()
42+
})
43+
}
44+
45+
func TestDurationError(t *testing.T) {
46+
for _, v := range []time.Duration{time.Microsecond, time.Second, time.Minute, time.Hour, 100 * time.Hour, 10000 * time.Hour} {
47+
for i := 0; i < 1000; i++ {
48+
d := time.Duration(rand.Int64N(int64(v)))
49+
s := string(Duration(d))
50+
d1, err := time.ParseDuration(s)
51+
if err != nil {
52+
t.Fatalf("%s: could not parse duration %q: %v", d, s, err)
53+
}
54+
if d1 < 89*d/100 || d1 > 111*d/100 {
55+
t.Fatalf("%s -> %s -> %s error is too large\n", d, s, d1)
56+
}
57+
}
58+
}
59+
}

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.3µs
17+
123.456µs -> 123.5µs
18+
1.234567ms -> 1.2ms
19+
5ms -> 5ms
20+
500ms -> 0.5s
21+
900ms -> 0.9s
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+
59s
35+
60s
36+
61s
37+
66s
38+
90s
39+
119s
40+
121s
41+
40m20s
42+
----
43+
15s -> 15s
44+
30s -> 30s
45+
31s -> 31s
46+
45s -> 45s
47+
59s -> 59s
48+
1m0s -> 1m
49+
1m1s -> 1m
50+
1m6s -> 1.1m
51+
1m30s -> 1.5m
52+
1m59s -> 2m
53+
2m1s -> 2m
54+
40m20s -> 40.3m
55+
56+
duration
57+
61m
58+
91m
59+
123m
60+
1234m
61+
12345m
62+
123456m
63+
1234567m
64+
12345678m
65+
123456789m
66+
----
67+
1h1m0s -> 1h
68+
1h31m0s -> 1.5h
69+
2h3m0s -> 2h
70+
20h34m0s -> 20.6h
71+
205h45m0s -> 206h
72+
2057h36m0s -> 2058h
73+
20576h7m0s -> 20576h
74+
205761h18m0s -> 205761h
75+
2057613h9m0s -> 2057613h
76+
77+
duration
78+
1h15m13s
79+
----
80+
1h15m13s -> 1.3h

0 commit comments

Comments
 (0)