si.go

  1package 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
 96// 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
106var 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}