si.go

  1package humanize
  2
  3import (
  4	"errors"
  5	"math"
  6	"regexp"
  7	"strconv"
  8)
  9
 10var 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
 34var revSIPrefixTable = revfmap(siPrefixTable)
 35
 36// revfmap reverses the map and precomputes the power multiplier
 37func 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
 45var riParseRegex *regexp.Regexp
 46
 47func 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")
 64func 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
 95func 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
105func SIWithDigits(input float64, decimals int, unit string) string {
106	value, prefix := ComputeSI(input)
107	return FtoaWithDigits(value, decimals) + " " + prefix + unit
108}
109
110var 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)
117func 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}