blob: ae659e0e49792e30e1f7ddcf149b145ee121f181 [file] [log] [blame]
khenaidoo59ce9dd2019-11-11 13:05:32 -05001package humanize
2
3import (
4 "errors"
5 "math"
6 "regexp"
7 "strconv"
8)
9
10var siPrefixTable = map[float64]string{
11 -24: "y", // yocto
12 -21: "z", // zepto
13 -18: "a", // atto
14 -15: "f", // femto
15 -12: "p", // pico
16 -9: "n", // nano
17 -6: "ยต", // micro
18 -3: "m", // milli
19 0: "",
20 3: "k", // kilo
21 6: "M", // mega
22 9: "G", // giga
23 12: "T", // tera
24 15: "P", // peta
25 18: "E", // exa
26 21: "Z", // zetta
27 24: "Y", // yotta
28}
29
30var revSIPrefixTable = revfmap(siPrefixTable)
31
32// revfmap reverses the map and precomputes the power multiplier
33func revfmap(in map[float64]string) map[string]float64 {
34 rv := map[string]float64{}
35 for k, v := range in {
36 rv[v] = math.Pow(10, k)
37 }
38 return rv
39}
40
41var riParseRegex *regexp.Regexp
42
43func init() {
44 ri := `^([\-0-9.]+)\s?([`
45 for _, v := range siPrefixTable {
46 ri += v
47 }
48 ri += `]?)(.*)`
49
50 riParseRegex = regexp.MustCompile(ri)
51}
52
53// ComputeSI finds the most appropriate SI prefix for the given number
54// and returns the prefix along with the value adjusted to be within
55// that prefix.
56//
57// See also: SI, ParseSI.
58//
59// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
60func ComputeSI(input float64) (float64, string) {
61 if input == 0 {
62 return 0, ""
63 }
64 mag := math.Abs(input)
65 exponent := math.Floor(logn(mag, 10))
66 exponent = math.Floor(exponent/3) * 3
67
68 value := mag / math.Pow(10, exponent)
69
70 // Handle special case where value is exactly 1000.0
71 // Should return 1 M instead of 1000 k
72 if value == 1000.0 {
73 exponent += 3
74 value = mag / math.Pow(10, exponent)
75 }
76
77 value = math.Copysign(value, input)
78
79 prefix := siPrefixTable[exponent]
80 return value, prefix
81}
82
83// SI returns a string with default formatting.
84//
85// SI uses Ftoa to format float value, removing trailing zeros.
86//
87// See also: ComputeSI, ParseSI.
88//
89// e.g. SI(1000000, "B") -> 1 MB
90// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
91func SI(input float64, unit string) string {
92 value, prefix := ComputeSI(input)
93 return Ftoa(value) + " " + prefix + unit
94}
95
khenaidoo26721882021-08-11 17:42:52 -040096// SIWithDigits works like SI but limits the resulting string to the
97// given number of decimal places.
98//
99// e.g. SIWithDigits(1000000, 0, "B") -> 1 MB
100// e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF
101func SIWithDigits(input float64, decimals int, unit string) string {
102 value, prefix := ComputeSI(input)
103 return FtoaWithDigits(value, decimals) + " " + prefix + unit
104}
105
khenaidoo59ce9dd2019-11-11 13:05:32 -0500106var errInvalid = errors.New("invalid input")
107
108// ParseSI parses an SI string back into the number and unit.
109//
110// See also: SI, ComputeSI.
111//
112// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
113func ParseSI(input string) (float64, string, error) {
114 found := riParseRegex.FindStringSubmatch(input)
115 if len(found) != 4 {
116 return 0, "", errInvalid
117 }
118 mag := revSIPrefixTable[found[2]]
119 unit := found[3]
120
121 base, err := strconv.ParseFloat(found[1], 64)
122 return base * mag, unit, err
123}