From 0328878e46da5d8ef8096dc519c7c1f9780c129d Mon Sep 17 00:00:00 2001 From: albertony <12441419+albertony@users.noreply.github.com> Date: Fri, 19 Aug 2022 11:40:43 +0200 Subject: [PATCH] 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 --- fs/accounting/stats.go | 2 +- fs/accounting/stats_test.go | 4 +-- fs/parseduration.go | 27 ++++++++++++++++++-- fs/parseduration_test.go | 50 +++++++++++++++++++++---------------- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/fs/accounting/stats.go b/fs/accounting/stats.go index f808e1fdf..51d910767 100644 --- a/fs/accounting/stats.go +++ b/fs/accounting/stats.go @@ -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 diff --git a/fs/accounting/stats_test.go b/fs/accounting/stats_test.go index b1a3cbb2a..58e8150bd 100644 --- a/fs/accounting/stats_test.go +++ b/fs/accounting/stats_test.go @@ -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) { diff --git a/fs/parseduration.go b/fs/parseduration.go index fe7d09986..3838d811c 100644 --- a/fs/parseduration.go +++ b/fs/parseduration.go @@ -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 diff --git a/fs/parseduration_test.go b/fs/parseduration_test.go index 4a5dbb16c..8361fa3bd 100644 --- a/fs/parseduration_test.go +++ b/fs/parseduration_test.go @@ -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) } } }