1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
|
// Copyright 2015, Joe Tsai. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file.
// Package unitconv implements string conversion functionality for
// unit prefixes such as those from the SI and IEC standards.
//
// See https://wikipedia.org/wiki/unit_prefix
package unitconv
import (
"errors"
"math"
"strconv"
"strings"
)
// Mode is the base conversion for strings.
type Mode int
// These are the different modes for Prefix string conversion.
const (
// AutoParse will parse the input as either the SI, IEC, or regular float
// notation as accepted by ParseFloat.
//
// This is an invalid mode for Append and Format.
AutoParse Mode = iota
// SI uses a scaling of 1000x, and uses a single letter symbol to denote
// the scaling. The prefixes ranges from Yocto (1E-24) to Yotta (1E+24).
//
// The output uses SI prefixes, and uses 'k' for Kilo and 'μ' for Micro.
// The input accepts SI prefixes along with 'k' and 'K' for Kilo and
// 'μ' and 'u' for Micro.
// It does not support the Deca, Hecto, Deci, or Centi prefixes.
SI
// Base1024 uses a scaling of 1024x, but uses the same prefixes as SI. This
// is a non-standard prefix notation and exists because many legacy systems
// unfortunately operate in this way.
//
// The output uses SI prefixes, but uses 'K' for Kilo and 'u' for Micro.
// The input accepts SI prefixes along with 'k' and 'K' for Kilo and
// 'μ' and 'u' for Micro. It also accepts IEC notation.
Base1024
// IEC uses a scaling of 1024x, and uses a two-letter symbol to denote the
// scaling in a system similar to SI. Instead of Kilo denoted as 'k', it
// uses Kibi denoted as 'Ki'. The prefixes ranges from Unit (1) to Yobi
// (1<<80). Representing values less than 1 will not use any of the divisor
// prefixes. IEC notation is easy to identify since the prefix symbols
// always end with the letter 'i'.
//
// The output uses IEC prefixes that are Unit and greater.
// The input accepts IEC prefixes only.
IEC
)
func (m Mode) base() float64 {
switch m {
case SI:
return 1000.0
case Base1024, IEC:
return 1024.0
default:
return math.NaN()
}
}
// Prefix factors according to IEC standards.
const (
// These are not standard IEC prefixes, but used internally.
yocbi = 1.0 / (1 << 80)
zepbi = 1.0 / (1 << 70)
attbi = 1.0 / (1 << 60)
fembi = 1.0 / (1 << 50)
picbi = 1.0 / (1 << 40)
nanbi = 1.0 / (1 << 30)
micbi = 1.0 / (1 << 20)
milbi = 1.0 / (1 << 10)
_ = 1 << 0
Kibi = 1 << 10
Mebi = 1 << 20
Gibi = 1 << 30
Tebi = 1 << 40
Pebi = 1 << 50
Exbi = 1 << 60
Zebi = 1 << 70
Yobi = 1 << 80
)
// Prefix factors according to SI standards.
const (
Yocto = 1E-24
Zepto = 1E-21
Atto = 1E-18
Femto = 1E-15
Pico = 1E-12
Nano = 1E-9
Micro = 1E-6
Milli = 1E-3
Unit = 1E0 // Not a standard SI prefix.
Kilo = 1E+3
Mega = 1E+6
Giga = 1E+9
Tera = 1E+12
Peta = 1E+15
Exa = 1E+18
Zetta = 1E+21
Yotta = 1E+24
)
const (
prefixes = divPrefixes + string(unitPrefix) + mulPrefixes
divPrefixes = "yzafpnum"
unitPrefix = '.'
mulPrefixes = "KMGTPEZY"
parsePrefixes = divPrefixes + mulPrefixes + string(kiloAlt) + string(microAlt)
maxExp = +len(mulPrefixes)
minExp = -len(divPrefixes)
microNorm, kiloNorm = 'u', 'K'
microAlt, kiloAlt = 'μ', 'k'
)
var (
mapNormToAlt = map[rune]rune{microNorm: microAlt, kiloNorm: kiloAlt}
mapAltToNorm = map[rune]rune{microAlt: microNorm, kiloAlt: kiloNorm}
scaleSI = []float64{
Yocto, Zepto, Atto, Femto, Pico, Nano, Micro, Milli,
Unit,
Kilo, Mega, Giga, Tera, Peta, Exa, Zetta, Yotta,
}
scaleIEC = []float64{
yocbi, zepbi, attbi, fembi, picbi, nanbi, micbi, milbi,
Unit,
Kibi, Mebi, Gibi, Tebi, Pebi, Exbi, Zebi, Yobi,
}
)
// Using a combination of the math.LogX and math.Pow functions can be lossy.
// This leads to slightly wrong values around the prefix boundaries. Thus, we
// look up the computed exponent in an authoritative list of scalings and
// adjust accordingly. We only check up to 3 values in the relevant section,
// ensuring a runtime of O(1).
func adjustLog(val float64, scales []float64, minExp, exp, maxExp int) int {
exp++
if exp > maxExp {
exp = maxExp
}
for exp >= minExp && scales[exp+len(scales)/2] > math.Abs(val) {
exp--
}
if exp < minExp {
exp = minExp
}
return exp
}
// AppendPrefix appends the string form of the floating-point number val, as
// generated by FormatPrefix, to dst and returns the extended buffer.
func AppendPrefix(dst []byte, val float64, m Mode, prec int) (out []byte) {
if math.IsNaN(val) || math.IsInf(val, 0) {
return strconv.AppendFloat(dst, val, 'f', -1, 64)
}
// Compute the prefix exponent.
var exp int
var adjustValue bool
if val != 0 {
switch m {
case SI:
exp = int(math.Floor(math.Log10(math.Abs(val)) / 3))
exp = adjustLog(val, scaleSI, minExp, exp, maxExp)
val /= scaleSI[exp+len(scaleSI)/2] // Same as: Pow(1000, exp)
adjustValue = minExp < exp && exp < maxExp
case Base1024:
exp = int(math.Floor(math.Log2(math.Abs(val)) / 10))
exp = adjustLog(val, scaleIEC, minExp, exp, maxExp)
val /= scaleIEC[exp+len(scaleSI)/2] // Same as: Pow(1024, exp)
adjustValue = minExp < exp && exp < maxExp
case IEC:
exp = int(math.Floor(math.Log2(math.Abs(val)) / 10))
exp = adjustLog(val, scaleIEC, 0, exp, maxExp)
val /= scaleIEC[exp+len(scaleSI)/2] // Same as: Pow(1024, exp)
adjustValue = 0 < exp && exp < maxExp
default:
return strconv.AppendInt(append(dst, '%'), int64(m), 10)
}
} else {
exp = 0
}
if adjustValue {
if math.Abs(val) < 1 {
val = math.Copysign(1, val)
}
if math.Abs(val) >= m.base() {
val = math.Copysign(math.Nextafter(m.base(), 0), val)
}
}
// Print the actual number.
dst = strconv.AppendFloat(dst, val, 'f', prec, 64)
// Print the prefix symbol.
if exp != 0 {
sym := prefixes[exp+len(prefixes)/2]
if alt := mapNormToAlt[rune(sym)]; m == SI && alt != 0 {
dst = append(dst, string(alt)...)
} else {
dst = append(dst, sym)
}
if m == IEC {
dst = append(dst, 'i')
}
}
return dst
}
// FormatPrefix converts the floating-point number val to a string, according
// to the prefix notation specified by mode. The prec specifies the precision
// used by the numeric portion.
//
// Even if prec is -1, formatting a value and parsing it does not guarantee that
// the exact value will be returned. It will however be extremely accurate.
//
// It is valid to format +Inf, -Inf, and NaN.
func FormatPrefix(val float64, m Mode, prec int) (str string) {
return string(AppendPrefix(nil, val, m, prec))
}
// ParsePrefix converts the string str to a floating-point number, according to
// the prefix notation specified by mode.
//
// It is valid to parse +Inf, -Inf, and NaN.
func ParsePrefix(str string, m Mode) (val float64, err error) {
val, err = strconv.ParseFloat(str, 64)
if err == nil && (m == AutoParse || math.IsNaN(val) || math.IsInf(val, 0)) {
return val, nil
}
err = nil // Reset the error
// If mode is AutoParse, detect the format to use.
if m == AutoParse {
if len(str) > 0 && str[len(str)-1] == 'i' {
m = IEC
} else {
m = SI
}
}
// Parse the prefix symbol.
var exp int
var saveStr = str
if i := strings.IndexAny(str, parsePrefixes); i >= 0 {
strPre := str[i:]
str = str[:i]
if m == IEC && len(strPre) != 2 {
goto fail
}
for si, ch := range strPre {
switch si {
case 0:
norm, usedAlt := mapAltToNorm[ch]
if usedAlt {
ch = norm
}
exp = strings.IndexByte(prefixes, byte(ch)) - len(prefixes)/2
if m == IEC && (usedAlt || exp < 0) {
goto fail
}
case 1:
if m == SI || ch != 'i' {
goto fail
}
default:
goto fail
}
}
}
// Parse the number part.
if strings.Trim(str, "-+.0123456789") != "" {
goto fail
}
if val, err = strconv.ParseFloat(str, 64); err != nil {
goto fail
}
// Compute the actual value.
switch m {
case SI:
return val * scaleSI[exp+len(scaleSI)/2], nil
case Base1024, IEC:
return val * scaleIEC[exp+len(scaleSI)/2], nil
default:
return 0, errors.New("unitconv: invalid mode")
}
fail:
if err == nil {
err = strconv.ErrSyntax
}
if nerr, ok := err.(*strconv.NumError); ok {
err = nerr.Err
}
err = &strconv.NumError{Func: "ParsePrefix", Num: saveStr, Err: err}
return val, err
}
|