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