accounting: limit length of ETA string

No need to report hours, minutes, and even seconds when the
ETA is several years, e.g. "292y24w3d23h47m16s". Now only
reports the 3 most significant units, sacrificing precision,
e.g. "292y24w3d", "24w3d23h", "3d23h47m", "23h47m16s".

Fixes #6381
This commit is contained in:
albertony 2022-08-19 11:40:43 +02:00
parent 67132ecaec
commit 0328878e46
4 changed files with 56 additions and 27 deletions

View File

@ -269,7 +269,7 @@ func etaString(done, total int64, rate float64) string {
if d == etaMax {
return "-"
}
return fs.Duration(d).ReadableString()
return fs.Duration(d).ShortReadableString()
}
// percent returns a/b as a percentage rounded to the nearest integer

View File

@ -29,7 +29,7 @@ func TestETA(t *testing.T) {
{size: 0, total: 15 * 86400, rate: 1.0, wantETA: 15 * 86400 * time.Second, wantOK: true, wantString: "2w1d"},
// Composite Custom String Cases
{size: 0, total: 1.5 * 86400, rate: 1.0, wantETA: 1.5 * 86400 * time.Second, wantOK: true, wantString: "1d12h"},
{size: 0, total: 95000, rate: 1.0, wantETA: 95000 * time.Second, wantOK: true, wantString: "1d2h23m20s"},
{size: 0, total: 95000, rate: 1.0, wantETA: 95000 * time.Second, wantOK: true, wantString: "1d2h23m"}, // Short format, if full it would be "1d2h23m20s"
// Standard Duration String Cases
{size: 0, total: 1, rate: 2.0, wantETA: 0, wantOK: true, wantString: "0s"},
{size: 0, total: 1, rate: 1.0, wantETA: time.Second, wantOK: true, wantString: "1s"},
@ -47,7 +47,7 @@ func TestETA(t *testing.T) {
// Extreme Cases
{size: 0, total: (1 << 63) - 1, rate: 1.0, wantETA: (time.Duration((1<<63)-1) / time.Second) * time.Second, wantOK: true, wantString: "-"},
{size: 0, total: ((1 << 63) - 1) / int64(time.Second), rate: 1.0, wantETA: (time.Duration((1<<63)-1) / time.Second) * time.Second, wantOK: true, wantString: "-"},
{size: 0, total: ((1<<63)-1)/int64(time.Second) - 1, rate: 1.0, wantETA: (time.Duration((1<<63)-1)/time.Second - 1) * time.Second, wantOK: true, wantString: "292y24w3d23h47m15s"},
{size: 0, total: ((1<<63)-1)/int64(time.Second) - 1, rate: 1.0, wantETA: (time.Duration((1<<63)-1)/time.Second - 1) * time.Second, wantOK: true, wantString: "292y24w3d"}, // Short format, if full it would be "292y24w3d23h47m15s"
{size: 0, total: ((1<<63)-1)/int64(time.Second) - 1, rate: 0.1, wantETA: (time.Duration((1<<63)-1) / time.Second) * time.Second, wantOK: true, wantString: "-"},
} {
t.Run(fmt.Sprintf("size=%d/total=%d/rate=%f", test.size, test.total, test.rate), func(t *testing.T) {

View File

@ -129,9 +129,27 @@ func ParseDuration(age string) (time.Duration, error) {
return parseDurationFromNow(age, timeNowFunc)
}
// ReadableString parses d into a human-readable duration.
// Based on https://github.com/hako/durafmt
// ReadableString parses d into a human-readable duration with units.
// Examples: "3s", "1d2h23m20s", "292y24w3d23h47m16s".
func (d Duration) ReadableString() string {
return d.readableString(0)
}
// ShortReadableString parses d into a human-readable duration with units.
// This method returns it in short format, including the 3 most significant
// units only, sacrificing precision if necessary. E.g. returns "292y24w3d"
// instead of "292y24w3d23h47m16s", and "3d23h47m" instead of "3d23h47m16s".
func (d Duration) ShortReadableString() string {
return d.readableString(3)
}
// readableString parses d into a human-readable duration with units.
// Parameter maxNumberOfUnits limits number of significant units to include,
// sacrificing precision. E.g. with argument 3 it returns "292y24w3d" instead
// of "292y24w3d23h47m16s", and "3d23h47m" instead of "3d23h47m16s". Zero or
// negative argument means include all.
// Based on https://github.com/hako/durafmt
func (d Duration) readableString(maxNumberOfUnits int) string {
switch d {
case DurationOff:
return "off"
@ -179,6 +197,7 @@ func (d Duration) ReadableString() string {
}
// Construct duration string.
numberOfUnits := 0
for _, u := range [...]string{"y", "w", "d", "h", "m", "s", "ms"} {
v := durationMap[u]
strval := strconv.FormatInt(v, 10)
@ -186,6 +205,10 @@ func (d Duration) ReadableString() string {
continue
}
readableString += strval + u
numberOfUnits++
if maxNumberOfUnits > 0 && numberOfUnits >= maxNumberOfUnits {
break
}
}
return readableString

View File

@ -108,38 +108,44 @@ func TestDurationString(t *testing.T) {
func TestDurationReadableString(t *testing.T) {
for _, test := range []struct {
negative bool
in time.Duration
want string
negative bool
in time.Duration
wantLong string
wantShort string
}{
// Edge Cases
{false, time.Duration(DurationOff), "off"},
{false, time.Duration(DurationOff), "off", "off"},
// Base Cases
{false, time.Duration(0), "0s"},
{true, time.Millisecond, "1ms"},
{true, time.Second, "1s"},
{true, time.Minute, "1m"},
{true, (3 * time.Minute) / 2, "1m30s"},
{true, time.Hour, "1h"},
{true, time.Hour * 24, "1d"},
{true, time.Hour * 24 * 7, "1w"},
{true, time.Hour * 24 * 365, "1y"},
{false, time.Duration(0), "0s", "0s"},
{true, time.Millisecond, "1ms", "1ms"},
{true, time.Second, "1s", "1s"},
{true, time.Minute, "1m", "1m"},
{true, (3 * time.Minute) / 2, "1m30s", "1m30s"},
{true, time.Hour, "1h", "1h"},
{true, time.Hour * 24, "1d", "1d"},
{true, time.Hour * 24 * 7, "1w", "1w"},
{true, time.Hour * 24 * 365, "1y", "1y"},
// Composite Cases
{true, time.Hour + 2*time.Minute + 3*time.Second, "1h2m3s"},
{true, time.Hour * 24 * (365 + 14), "1y2w"},
{true, time.Hour*24*4 + time.Hour*3 + time.Minute*2 + time.Second, "4d3h2m1s"},
{true, time.Hour * 24 * (365*3 + 7*2 + 1), "3y2w1d"},
{true, time.Hour*24*(365*3+7*2+1) + time.Hour*2 + time.Second, "3y2w1d2h1s"},
{true, time.Hour*24*(365*3+7*2+1) + time.Second, "3y2w1d1s"},
{true, time.Hour*24*(365+7*2+3) + time.Hour*4 + time.Minute*5 + time.Second*6 + time.Millisecond*7, "1y2w3d4h5m6s7ms"},
{true, time.Hour + 2*time.Minute + 3*time.Second, "1h2m3s", "1h2m3s"},
{true, time.Hour * 24 * (365 + 14), "1y2w", "1y2w"},
{true, time.Hour*24*4 + time.Hour*3 + time.Minute*2 + time.Second, "4d3h2m1s", "4d3h2m"},
{true, time.Hour * 24 * (365*3 + 7*2 + 1), "3y2w1d", "3y2w1d"},
{true, time.Hour*24*(365*3+7*2+1) + time.Hour*2 + time.Second, "3y2w1d2h1s", "3y2w1d"},
{true, time.Hour*24*(365*3+7*2+1) + time.Second, "3y2w1d1s", "3y2w1d"},
{true, time.Hour*24*(365+7*2+3) + time.Hour*4 + time.Minute*5 + time.Second*6 + time.Millisecond*7, "1y2w3d4h5m6s7ms", "1y2w3d"},
{true, time.Duration(DurationOff) / time.Millisecond * time.Millisecond, "292y24w3d23h47m16s853ms", "292y24w3d"}, // Should have been 854ms but some precision are lost with floating point calculations
} {
got := Duration(test.in).ReadableString()
assert.Equal(t, test.want, got)
assert.Equal(t, test.wantLong, got)
got = Duration(test.in).ShortReadableString()
assert.Equal(t, test.wantShort, got)
// Test Negative Case
if test.negative {
got = Duration(-test.in).ReadableString()
assert.Equal(t, "-"+test.want, got)
assert.Equal(t, "-"+test.wantLong, got)
got = Duration(-test.in).ShortReadableString()
assert.Equal(t, "-"+test.wantShort, got)
}
}
}