diff --git a/fs/accounting/stats.go b/fs/accounting/stats.go index 8c7e2db8f..0dbcfaf5b 100644 --- a/fs/accounting/stats.go +++ b/fs/accounting/stats.go @@ -172,7 +172,7 @@ func etaString(done, total int64, rate float64) string { if !ok { return "-" } - return d.String() + return fs.Duration(d).ReadableString() } // 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 af5eeebcd..8db584ed6 100644 --- a/fs/accounting/stats_test.go +++ b/fs/accounting/stats_test.go @@ -19,9 +19,20 @@ func TestETA(t *testing.T) { wantOK bool wantString string }{ + // Custom String Cases + {size: 0, total: 365 * 86400, rate: 1.0, wantETA: 365 * 86400 * time.Second, wantOK: true, wantString: "1y"}, + {size: 0, total: 7 * 86400, rate: 1.0, wantETA: 7 * 86400 * time.Second, wantOK: true, wantString: "1w"}, + {size: 0, total: 1 * 86400, rate: 1.0, wantETA: 1 * 86400 * time.Second, wantOK: true, wantString: "1d"}, + {size: 0, total: 1110 * 86400, rate: 1.0, wantETA: 1110 * 86400 * time.Second, wantOK: true, wantString: "3y2w1d"}, + {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"}, + // Standard Duration String Cases {size: 0, total: 100, rate: 1.0, wantETA: 100 * time.Second, wantOK: true, wantString: "1m40s"}, {size: 50, total: 100, rate: 1.0, wantETA: 50 * time.Second, wantOK: true, wantString: "50s"}, {size: 100, total: 100, rate: 1.0, wantETA: 0 * time.Second, wantOK: true, wantString: "0s"}, + // No String Cases {size: -1, total: 100, rate: 1.0, wantETA: 0, wantOK: false, wantString: "-"}, {size: 200, total: 100, rate: 1.0, wantETA: 0, wantOK: false, wantString: "-"}, {size: 10, total: -1, rate: 1.0, wantETA: 0, wantOK: false, wantString: "-"}, diff --git a/fs/parseduration.go b/fs/parseduration.go index 94626a1fe..e79b323ee 100644 --- a/fs/parseduration.go +++ b/fs/parseduration.go @@ -78,6 +78,68 @@ func ParseDuration(age string) (time.Duration, error) { return time.Duration(period), nil } +// ReadableString parses d into a human readable duration. +// Based on https://github.com/hako/durafmt +func (d Duration) ReadableString() string { + switch d { + case DurationOff: + return "off" + case 0: + return "0s" + } + + readableString := "" + + // Check for minus durations. + if d < 0 { + readableString += "-" + } + + duration := time.Duration(math.Abs(float64(d))) + + // Convert duration. + seconds := int64(duration.Seconds()) % 60 + minutes := int64(duration.Minutes()) % 60 + hours := int64(duration.Hours()) % 24 + days := int64(duration/(24*time.Hour)) % 365 % 7 + + // Edge case between 364 and 365 days. + // We need to calculate weeks from what is left from years + leftYearDays := int64(duration/(24*time.Hour)) % 365 + weeks := leftYearDays / 7 + if leftYearDays >= 364 && leftYearDays < 365 { + weeks = 52 + } + + years := int64(duration/(24*time.Hour)) / 365 + milliseconds := int64(duration/time.Millisecond) - + (seconds * 1000) - (minutes * 60000) - (hours * 3600000) - + (days * 86400000) - (weeks * 604800000) - (years * 31536000000) + + // Create a map of the converted duration time. + durationMap := map[string]int64{ + "ms": milliseconds, + "s": seconds, + "m": minutes, + "h": hours, + "d": days, + "w": weeks, + "y": years, + } + + // Construct duration string. + for _, u := range [...]string{"y", "w", "d", "h", "m", "s", "ms"} { + v := durationMap[u] + strval := strconv.FormatInt(v, 10) + if v == 0 { + continue + } + readableString += strval + u + } + + return readableString +} + // Set a Duration func (d *Duration) Set(s string) error { duration, err := ParseDuration(s) diff --git a/fs/parseduration_test.go b/fs/parseduration_test.go index 3a486549b..49ff164c9 100644 --- a/fs/parseduration_test.go +++ b/fs/parseduration_test.go @@ -84,6 +84,44 @@ func TestDurationString(t *testing.T) { } } +func TestDurationReadableString(t *testing.T) { + for _, test := range []struct { + negative bool + in time.Duration + want string + }{ + // Edge Cases + {false, time.Duration(DurationOff), "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"}, + // 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"}, + } { + got := Duration(test.in).ReadableString() + assert.Equal(t, test.want, got) + + // Test Negative Case + if test.negative { + got = Duration(-test.in).ReadableString() + assert.Equal(t, "-"+test.want, got) + } + } +} + func TestDurationScan(t *testing.T) { var v Duration n, err := fmt.Sscan(" 17m ", &v)