diff --git a/src/app/pc_string.go b/src/app/pc_string.go index 04f1f5b..03a8340 100644 --- a/src/app/pc_string.go +++ b/src/app/pc_string.go @@ -10,21 +10,52 @@ func isStringDefined(str string) bool { return len(strings.TrimSpace(str)) > 0 } -func durationToString(dur time.Duration) string { - if dur.Minutes() < 3 { - //seconds - return dur.Round(time.Second).String() - } else if dur.Minutes() < 60 { - //minutes - return fmt.Sprintf("%.0fm", dur.Minutes()) - } else if dur.Hours() < 24 { - //hours and minutes - return fmt.Sprintf("%dh%dm", int(dur.Hours()), int(dur.Minutes())%60) - } else if dur.Hours() < 48 { - //days and hours - return fmt.Sprintf("%dd%dh", int(dur.Hours())/24, int(dur.Hours())%24) - } else { - //days - return fmt.Sprintf("%dd", int(dur.Hours())/24) +// HumanDuration returns a succinct representation of the provided duration +// with limited precision for consumption by humans. It provides ~2-3 significant +// figures of duration. +func HumanDuration(d time.Duration) string { + // Allow deviation no more than 2 seconds(excluded) to tolerate machine time + // inconsistence, it can be considered as almost now. + if seconds := int(d.Seconds()); seconds < -1 { + return "" + } else if seconds < 0 { + return "0s" + } else if seconds < 60*2 { + return fmt.Sprintf("%ds", seconds) } + minutes := int(d / time.Minute) + if minutes < 10 { + s := int(d/time.Second) % 60 + if s == 0 { + return fmt.Sprintf("%dm", minutes) + } + return fmt.Sprintf("%dm%ds", minutes, s) + } else if minutes < 60*3 { + return fmt.Sprintf("%dm", minutes) + } + hours := int(d / time.Hour) + if hours < 8 { + m := int(d/time.Minute) % 60 + if m == 0 { + return fmt.Sprintf("%dh", hours) + } + return fmt.Sprintf("%dh%dm", hours, m) + } else if hours < 48 { + return fmt.Sprintf("%dh", hours) + } else if hours < 24*8 { + h := hours % 24 + if h == 0 { + return fmt.Sprintf("%dd", hours/24) + } + return fmt.Sprintf("%dd%dh", hours/24, h) + } else if hours < 24*365*2 { + return fmt.Sprintf("%dd", hours/24) + } else if hours < 24*365*8 { + dy := int(hours/24) % 365 + if dy == 0 { + return fmt.Sprintf("%dy", hours/24/365) + } + return fmt.Sprintf("%dy%dd", hours/24/365, dy) + } + return fmt.Sprintf("%dy", int(hours/24/365)) } diff --git a/src/app/pc_string_test.go b/src/app/pc_string_test.go new file mode 100644 index 0000000..b1757a6 --- /dev/null +++ b/src/app/pc_string_test.go @@ -0,0 +1,83 @@ +package app + +import ( + "testing" + "time" +) + +func TestHumanDuration(t *testing.T) { + tests := []struct { + d time.Duration + want string + }{ + {d: time.Second, want: "1s"}, + {d: 70 * time.Second, want: "70s"}, + {d: 190 * time.Second, want: "3m10s"}, + {d: 70 * time.Minute, want: "70m"}, + {d: 47 * time.Hour, want: "47h"}, + {d: 49 * time.Hour, want: "2d1h"}, + {d: (8*24 + 2) * time.Hour, want: "8d"}, + {d: (367 * 24) * time.Hour, want: "367d"}, + {d: (365*2*24 + 25) * time.Hour, want: "2y1d"}, + {d: (365*8*24 + 2) * time.Hour, want: "8y"}, + } + for _, tt := range tests { + t.Run(tt.d.String(), func(t *testing.T) { + if got := HumanDuration(tt.d); got != tt.want { + t.Errorf("HumanDuration() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHumanDurationBoundaries(t *testing.T) { + tests := []struct { + d time.Duration + want string + }{ + {d: -2 * time.Second, want: ""}, + {d: -2*time.Second + 1, want: "0s"}, + {d: 0, want: "0s"}, + {d: time.Second - time.Millisecond, want: "0s"}, + {d: 2*time.Minute - time.Millisecond, want: "119s"}, + {d: 2 * time.Minute, want: "2m"}, + {d: 2*time.Minute + time.Second, want: "2m1s"}, + {d: 10*time.Minute - time.Millisecond, want: "9m59s"}, + {d: 10 * time.Minute, want: "10m"}, + {d: 10*time.Minute + time.Second, want: "10m"}, + {d: 3*time.Hour - time.Millisecond, want: "179m"}, + {d: 3 * time.Hour, want: "3h"}, + {d: 3*time.Hour + time.Minute, want: "3h1m"}, + {d: 8*time.Hour - time.Millisecond, want: "7h59m"}, + {d: 8 * time.Hour, want: "8h"}, + {d: 8*time.Hour + 59*time.Minute, want: "8h"}, + {d: 2*24*time.Hour - time.Millisecond, want: "47h"}, + {d: 2 * 24 * time.Hour, want: "2d"}, + {d: 2*24*time.Hour + time.Hour, want: "2d1h"}, + {d: 8*24*time.Hour - time.Millisecond, want: "7d23h"}, + {d: 8 * 24 * time.Hour, want: "8d"}, + {d: 8*24*time.Hour + 23*time.Hour, want: "8d"}, + {d: 2*365*24*time.Hour - time.Millisecond, want: "729d"}, + {d: 2 * 365 * 24 * time.Hour, want: "2y"}, + {d: 2*365*24*time.Hour + 23*time.Hour, want: "2y"}, + {d: 2*365*24*time.Hour + 23*time.Hour + 59*time.Minute, want: "2y"}, + {d: 2*365*24*time.Hour + 24*time.Hour - time.Millisecond, want: "2y"}, + {d: 2*365*24*time.Hour + 24*time.Hour, want: "2y1d"}, + {d: 3 * 365 * 24 * time.Hour, want: "3y"}, + {d: 4 * 365 * 24 * time.Hour, want: "4y"}, + {d: 5 * 365 * 24 * time.Hour, want: "5y"}, + {d: 6 * 365 * 24 * time.Hour, want: "6y"}, + {d: 7 * 365 * 24 * time.Hour, want: "7y"}, + {d: 8*365*24*time.Hour - time.Millisecond, want: "7y364d"}, + {d: 8 * 365 * 24 * time.Hour, want: "8y"}, + {d: 8*365*24*time.Hour + 364*24*time.Hour, want: "8y"}, + {d: 9 * 365 * 24 * time.Hour, want: "9y"}, + } + for _, tt := range tests { + t.Run(tt.d.String(), func(t *testing.T) { + if got := HumanDuration(tt.d); got != tt.want { + t.Errorf("HumanDuration() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/app/process.go b/src/app/process.go index 3f2bf76..88b2cc6 100644 --- a/src/app/process.go +++ b/src/app/process.go @@ -531,7 +531,7 @@ func (p *Process) updateProcState() { defer p.stateMtx.Unlock() if isRunning { dur := time.Since(p.getStartTime()) - p.procState.SystemTime = durationToString(dur) + p.procState.SystemTime = HumanDuration(dur) p.procState.Age = dur p.procState.Name = p.getName() p.procState.Mem, p.procState.CPU = p.getResourceUsage() diff --git a/src/app/process_test.go b/src/app/process_test.go deleted file mode 100644 index adaf62f..0000000 --- a/src/app/process_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package app - -import ( - "testing" - "time" -) - -func TestDurationToString(t *testing.T) { - type args struct { - dur time.Duration - } - tests := []struct { - name string - args args - want string - }{ - { - name: "milis", - args: args{ - dur: 20 * time.Millisecond, - }, - want: "0s", - }, - { - name: "under 1m", - args: args{ - dur: 20 * time.Second, - }, - want: "20s", - }, - { - name: "under 3m", - args: args{ - dur: 150 * time.Second, - }, - want: "2m30s", - }, - { - name: "under 1h", - args: args{ - dur: 30 * time.Minute, - }, - want: "30m", - }, - { - name: "under 24h", - args: args{ - dur: 280 * time.Minute, - }, - want: "4h40m", - }, - { - name: "above 24h", - args: args{ - dur: 25*time.Hour + 50*time.Minute, - }, - want: "1d1h", - }, - { - name: "above 48h", - args: args{ - dur: 49*time.Hour + 50*time.Minute, - }, - want: "2d", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := durationToString(tt.args.dur); got != tt.want { - t.Errorf("DurationToString() = %v, want %v", got, tt.want) - } - }) - } -}