-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathreportutils.go
204 lines (161 loc) · 5.39 KB
/
reportutils.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
package reportutils
// This package exposes a number of tools that facilitate consistent
// formatting in log reports.
import (
"fmt"
"math"
"strings"
"time"
"github.com/mongodb-labs/migration-verifier/internal/types"
"github.com/mongodb-labs/migration-verifier/internal/util"
"github.com/dustin/go-humanize"
"golang.org/x/exp/constraints"
)
const decimalPrecision = 2
// num16Plus is like realNum, but it excludes 8-bit int/uint.
type num16Plus interface {
constraints.Float |
~uint | ~uint16 | ~uint32 | ~uint64 |
~int | ~int16 | ~int32 | ~int64
}
type realNum interface {
constraints.Float | constraints.Integer
}
// DataUnit signifies some unit of data.
type DataUnit string
const (
Bytes DataUnit = "bytes"
KiB DataUnit = "KiB"
MiB DataUnit = "MiB"
GiB DataUnit = "GiB"
TiB DataUnit = "TiB"
PiB DataUnit = "PiB"
// Anything larger than the above seems like overkill.
)
var unitSize = map[DataUnit]uint64{
KiB: humanize.KiByte,
MiB: humanize.MiByte,
GiB: humanize.GiByte,
TiB: humanize.TiByte,
PiB: humanize.PiByte,
}
// DurationToHMS stringifies `duration` as, e.g., "1h 22m 3.23s".
// It’s a lot like Duration.String(), but with spaces between,
// and the lowest unit shown is always the second.
func DurationToHMS(duration time.Duration) string {
hours := int(math.Floor(duration.Hours()))
minutes := int(math.Floor(duration.Minutes())) % 60
secs := math.Mod(duration.Seconds(), 60)
str := FmtReal(secs) + "s"
if hours > 0 {
str = fmt.Sprintf("%dh %dm %s", hours, minutes, str)
} else if minutes > 0 {
str = fmt.Sprintf("%dm %s", minutes, str)
}
return str
}
// BytesToUnit returns a stringified number that represents `count`
// in the given `unit`. For example, count=1024 and unit=KiB would
// return "1".
func BytesToUnit[T num16Plus](count T, unit DataUnit) string {
// Ideally go-humanize could do this for us,
// but as of this writing it can’t.
// https://github.com/dustin/go-humanize/issues/111
// We could put Bytes into the unitSize map above and handle it
// the same as other units, but that would entail int/float
// conversion and possibly risk little rounding errors. We might
// as well keep it simple where we can.
if unit == Bytes {
return FmtReal(count)
}
myUnitSize, exists := unitSize[unit]
if !exists {
panic(fmt.Sprintf("Missing unit in unitSize: %s", unit))
}
return FmtReal(util.Divide(count, myUnitSize))
}
// FmtReal provides a standard formatting of real numbers, with a consistent
// precision and trailing decimal zeros removed.
func FmtReal[T types.RealNumber](num T) string {
switch any(num).(type) {
case float32, float64:
return fmtFloat(num)
case uint64, uint:
// Uints that can’t be int64 need to be formatted as floats.
if uint64(num) > math.MaxInt64 {
return fmtFloat(num)
}
// Any other uint* type can be an int, which we format below.
default:
// Formatted below.
}
return humanize.Comma(int64(num))
}
func fmtFloat[T types.RealNumber](num T) string {
return humanize.Commaf(roundFloat(float64(num), decimalPrecision))
}
func roundFloat(val float64, precision uint) float64 {
ratio := math.Pow10(int(precision))
return math.Round(val*ratio) / ratio
}
func fmtQuotient[T, U realNum](dividend T, divisor U) string {
return FmtReal(util.Divide(dividend, divisor))
}
// FmtPercent returns a stringified percentage without a trailing `%`,
// formatted as per FmtFloat(). FmtPercent also ensures that any
// percentage less than 100% is reported as something less; e.g.,
// 99.999997 doesn’t get rounded up to 100.
func FmtPercent[T, U realNum](numerator T, denominator U) string {
str := fmtQuotient(100*numerator, denominator)
// If the numerator & denominator are large then it’s possible
// for str to be “100” without the numbers actually being equal.
// For our purposes, though, “100” percent should mean the
// denominator cannot exceed the numerator.
//
// (For now it’s ok to return “100” if the numerator exceeds the
// denominator.)
if str == "100" && (U(numerator) != denominator) {
if U(numerator) < denominator {
return "99." + strings.Repeat("9", decimalPrecision)
}
}
return str
}
// FindBestUnit gives the “best” DataUnit for the given `count` of bytes.
//
// You can then give that DataUnit to BytesToUnit() to stringify
// multiple byte counts to the same unit.
func FindBestUnit[T num16Plus](count T) DataUnit {
// humanize.IBytes() does most of what we want but lacks the
// flexibility to specify a precision. It’s not complicated to
// implement here anyway.
if count < T(humanize.KiByte) {
return Bytes
}
// If the log2 is, e.g., 32.05, we want to use 2^30, i.e., GiB.
log2 := math.Log2(float64(count))
// Convert log2 to the next-lowest multiple of 10.
unitNum := 10 * uint64(math.Floor(log2/10))
// Now find that power of 2, which we can compare against
// the values of the unitSize map (above).
unitNum = 1 << unitNum
// Just in case, someday, exibytes become relevant …
var biggestSize uint64
var biggestUnit DataUnit
for unit, size := range unitSize {
if size == unitNum {
return unit
}
if size > biggestSize {
biggestSize = size
biggestUnit = unit
}
}
return biggestUnit
}
// FmtBytes is a convenience that combines BytesToUnit with FindBestUnit.
// Use it to format a single count of bytes.
func FmtBytes[T num16Plus](count T) string {
unit := FindBestUnit(count)
return BytesToUnit(count, unit) + " " + string(unit)
}