si.go raw

   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