Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions crhumanize/duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.

package crhumanize

import (
"fmt"
"time"
)

// Duration returns a simplified approximation (±5%) of a duration.
//
// Examples:
// - 123.456µs -> "123µs"
// - 1.234567ms -> "1.2ms"
// - 59.1s -> "59s"
// - 1m6.5s -> 1m7s
func Duration(d time.Duration) SafeString {
if d < 0 {
return "-" + Duration(-d)
}
if d == 0 {
return "0s"
}

if d < 10*time.Minute {
// Round to a precision that gives us one decimal when the value is a single
// digit (e.g. "1.5ms" vs "12ms")
r := time.Nanosecond
switch {
case d < time.Microsecond:
case d < 10*time.Microsecond:
r = 100 * time.Nanosecond
case d < time.Millisecond:
r = time.Microsecond
case d < 10*time.Millisecond:
r = 100 * time.Microsecond
case d < time.Second:
r = time.Millisecond
case d < 10*time.Second:
r = 100 * time.Millisecond
default:
r = time.Second
}
return SafeString(d.Round(r).String())
}

if d > 100*time.Hour {
d = d.Round(time.Hour)
} else {
d = d.Round(time.Minute)
}
h := int(d / time.Hour)
m := int((d % time.Hour) / time.Minute)
switch {
case h == 0:
return SafeString(fmt.Sprintf("%dm", m))
case m == 0:
return SafeString(fmt.Sprintf("%dh", h))
default:
return SafeString(fmt.Sprintf("%dh%dm", h, m))
}
}
60 changes: 60 additions & 0 deletions crhumanize/duration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2025 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.

package crhumanize

import (
"fmt"
"math"
"math/rand/v2"
"strings"
"testing"
"time"

"github.com/cockroachdb/crlib/crstrings"
"github.com/cockroachdb/crlib/internal/datadriven"
)

func TestDuration(t *testing.T) {
datadriven.RunTest(t, "testdata/duration", func(t *testing.T, td *datadriven.TestData) string {
if td.Cmd != "duration" {
td.Fatalf(t, "unknown command: %q", td.Cmd)
}
var buf strings.Builder
for _, l := range crstrings.Lines(td.Input) {
d, err := time.ParseDuration(l)
if err != nil {
td.Fatalf(t, "could not parse duration %q: %v", l, err)
}
fmt.Fprintf(&buf, "%s -> %s\n", d, Duration(d))
}
return buf.String()
})
}

func TestDurationError(t *testing.T) {
for _, v := range []time.Duration{time.Microsecond, time.Second, time.Minute, time.Hour, 100 * time.Hour, 10000 * time.Hour} {
for i := 0; i < 1000; i++ {
d := time.Duration(rand.Int64N(int64(v)))
s := string(Duration(d))
d1, err := time.ParseDuration(s)
if err != nil {
t.Fatalf("%s: could not parse duration %q: %v", d, s, err)
}
if relativeErr := math.Abs(float64(d1-d)) / float64(d); relativeErr > 0.05 {
t.Fatalf("%s -> %s -> %s error is too large: %f\n", d, s, d1, relativeErr)
}
}
}
}
80 changes: 80 additions & 0 deletions crhumanize/testdata/duration
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
duration
1ns
12345ns
123456ns
1234567ns
5ms
500ms
900ms
1s
1001ms
1011ms
1111ms
1500ms
----
1ns -> 1ns
12.345µs -> 12µs
123.456µs -> 123µs
1.234567ms -> 1.2ms
5ms -> 5ms
500ms -> 500ms
900ms -> 900ms
1s -> 1s
1.001s -> 1s
1.011s -> 1s
1.111s -> 1.1s
1.5s -> 1.5s


duration
15s
30s
31s
45s
59.1s
60s
61s
66.5s
90.2s
119s
121s
40m20s
----
15s -> 15s
30s -> 30s
31s -> 31s
45s -> 45s
59.1s -> 59s
1m0s -> 1m0s
1m1s -> 1m1s
1m6.5s -> 1m7s
1m30.2s -> 1m30s
1m59s -> 1m59s
2m1s -> 2m1s
40m20s -> 40m

duration
1h1m15s
1h31m13s
2h3m
20h34m50s
205h45m15s
2057h36m
20576h7m
205761h18m
2057613h9m
----
1h1m15s -> 1h1m
1h31m13s -> 1h31m
2h3m0s -> 2h3m
20h34m50s -> 20h35m
205h45m15s -> 206h
2057h36m0s -> 2058h
20576h7m0s -> 20576h
205761h18m0s -> 205761h
2057613h9m0s -> 2057613h

duration
1h15m13s
----
1h15m13s -> 1h15m
Loading