Source

units / fmt.go

Full commit
package units

import "strconv"

// Factor x into lowest prime factor p and everything else e.
func factor(x uint32) (p, e uint32) {
	var i uint32 = 2
	for ; x%i != 0 && i < 20; i++ {
	}
	return i, x / i
}

func unitString(u uint32, sep byte) string {
	var buf []byte
	var p uint32
	var seenUnit bool
	for u > 1 {
		if seenUnit {
			buf = append(buf, sep)
		}
		seenUnit = true

		p, u = factor(u)
		switch p {
		case meterPart:
			buf = append(buf, 'm')
		case secondPart:
			buf = append(buf, 's')
		case kilogramPart:
			buf = append(buf, 'k', 'g')
		case amperePart:
			buf = append(buf, 'A')
		case kelvinPart:
			buf = append(buf, 'K')
		case bytePart:
			buf = append(buf, 'B')
		default:
			return "?"
		}
	}
	return string(buf)
}

func (u unit) String() string {
	numer := unitString(u.numer, '*')
	denom := unitString(u.denom, '/')
	if numer == "?" || denom == "?" {
		return "?unit?"
	}
	if len(denom) > 0 {
		return numer + "/" + denom
	}
	return numer
}

var siUnits = map[unit]string{
	Meter:     "m",
	Second:    "s",
	Kilogram:  "kg",
	Newton:    "N",
	Joule:     "J",
	Watt:      "W",
	Pascal:    "Pa",
	Hertz:     "Hz",
	Kiloliter: "kL",

	Coulomb: "C",
	Ampere:  "A",
	Volt:    "V",
	Ohm:     "\u03a9",
	Farad:   "F",
	Weber:   "Wb",
	Henry:   "H",
	Tesla:   "T",

	Kelvin:   "K",
	Byte:     "B",
	Unitless: "",
}

var expPrefix = map[int]string{
	24:  "Y",
	21:  "Z",
	18:  "E",
	15:  "P",
	12:  "T",
	9:   "G",
	6:   "M",
	3:   "k",
	0:   "",
	-2:  "c",
	-3:  "m",
	-6:  "\u03bc",
	-9:  "n",
	-12: "p",
	-15: "f",
	-18: "a",
	-21: "z",
	-24: "y",
}

var timeUnits = []struct {
	symbol  string
	seconds float64
}{
	{"y", 3600 * 24 * 365.2425},
	{"d", 3600 * 24},
	{"h", 3600},
	{"min", 60},
	{"sec", 1},
}

func formatTime(sec float64) (out string) {
	for _, time := range timeUnits {
		if sec >= time.seconds {
			n := sec / time.seconds
			return strconv.FormatFloat(n, 'g', 4, 64) + time.symbol
		}
	}
	return "never"
}

// Factor x = z * 10^e, where 1 <= |z| < 1000 and e%3==0
func extractExp(x float64) (z float64, e int) {
	if x == 0 {
		return x, 0
	}
	if x != x {
		return x, 0
	}

	var neg bool = x < 0
	if neg {
		x = -x
	}
	var small bool = x < 1
	if small {
		x = 1 / x
	}

	for x >= 1000 && e < 24 {
		x /= 1000
		e += 3
	}

	if small {
		x = 1 / x
		e = -e
		if e != -24 && x < 1 {
			x *= 1000
			e -= 3
		}
	}
	if neg {
		x = -x
	}

	return x, e
}

func (x Value) String() string {
	n := x.num
	e := 0
	symbol, ok := siUnits[x.units]
	if !ok {
		symbol = x.units.String()
	} else {
		switch x.units {
		case Unitless:
			return strconv.FormatFloat(n, 'g', 6, 64)
		case Kilogram:
			n *= 1000
			symbol = "g"
		case Kiloliter:
			n *= 1000
			symbol = "L"
		case Second:
			if n >= 60 {
				return formatTime(n)
			}
		}
		n, e = extractExp(n)
		symbol = expPrefix[e] + symbol
	}
	return strconv.FormatFloat(n, 'g', 6, 64) + symbol
}

var prefixExp map[string]int
var namedUnits map[string]Value

func init() {
	prefixExp = map[string]int{
		"K":      3,
		"u":      -6,
		"\u00b5": -6,
	}
	for e, p := range expPrefix {
		if e != 0 {
			prefixExp[p] = e
		}
	}

	namedUnits = map[string]Value{
		"g":      Value{.001, Kilogram},
		"l":      Value{.001, Kiloliter},
		"L":      Value{.001, Kiloliter},
		"ohm":    Value{1, Ohm},
		"\u2126": Value{1, Ohm},

		"mi":  Value{1.609344e3, Meter},
		"ft":  Value{0.3048, Meter},
		"in":  Value{0.0254, Meter},
		"lbs": Value{0.45359237, Kilogram},
		"oz":  Value{0.0283495231, Kilogram},
		"gal": Value{0.003785412, Kiloliter},

		"mph": Value{1.609344e3 / 3600, Meter.per(Second)},
		"lbf": Value{4.448222, Newton},
		"hp":  Value{745.7, Watt},
		"Btu": Value{1055, Joule},

		"y":   Value{3600 * 24 * 365.2425, Second},
		"d":   Value{3600 * 24, Second},
		"h":   Value{3600, Second},
		"min": Value{60, Second},

		"b":   Value{.125, Byte},
		"bit": Value{.125, Byte},

		// Until I feel like implementing constants separately.
		"c": Value{299792458, Meter.per(Second)},
	}
	for u, n := range siUnits {
		namedUnits[n] = Value{1, u}
	}
	delete(namedUnits, "kg")
	delete(namedUnits, "kL")
}

func parseSimpleUnit(s string) (v Value, ok bool) {
	// get the longest valid suffix as the base unit
	i := 0
	for ; i < len(s); i++ {
		suf := s[i:]
		v, ok = namedUnits[suf]
		if ok {
			break
		}
	}
	if !ok || i == 0 {
		return
	}

	// Check for crazy binary prefixes
	binary := false
	if s[i-1] == 'i' && v.units == Byte {
		binary = true
		i--
	}

	prefix := s[:i]
	exp, ok := prefixExp[prefix]
	if !ok {
		return Value{}, false
	}

	if binary && exp < 3 {
		return Value{}, false
	}

	var mul float64 = 1000
	if binary {
		if exp < 3 {
			return Value{}, false
		} else {
			mul = 1024
		}
	}
	if exp < 0 {
		exp = -exp
		mul = .001
	}
	for exp > 0 {
		exp -= 3
		v.num *= mul
	}
	for exp < 0 {
		exp += 1
		v.num *= 10
	}
	return v, true
}

func parseUnit(s string) (v Value, ok bool) {
	v.num = 1
	v.units = Unitless
	start := 0
	numerator := true
	for i := 0; i <= len(s); i++ {
		if i == len(s) || s[i] == '/' || s[i] == '*' {
			val, ok := parseSimpleUnit(s[start:i])
			if !ok {
				return Value{}, false
			}
			if numerator {
				v = v.Mul(val)
			} else {
				v = v.Div(val)
			}
			start = i + 1
		}
		if i < len(s) {
			if s[i] == '/' {
				numerator = false
			} else if s[i] == '*' {
				numerator = true
			}
		}
	}
	return v, true
}