mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-29 06:28:57 -04:00
Package unitconv implements string conversion functionality for unit prefixes such as those from the SI and IEC standards. It is catered towards printing of values for human consumption. This exists to support open-sourcing JSON migration tooling. Updates #20220 Updates tailscale/corp#791 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
201 lines
5.6 KiB
Go
201 lines
5.6 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package unitconv implements string conversion functionality
|
|
// for unit prefixes such as those from the SI and IEC standards.
|
|
// It is catered towards printing of values for human consumption.
|
|
// Consequently it prints of reasonable precision proportional to
|
|
// the magnitude of the value itself.
|
|
package unitconv
|
|
|
|
import (
|
|
"bytes"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"golang.org/x/exp/constraints"
|
|
)
|
|
|
|
type numeric interface {
|
|
constraints.Integer | constraints.Float
|
|
}
|
|
|
|
type prefixDivisor struct {
|
|
prefix string
|
|
divisor float64
|
|
}
|
|
|
|
var prefixDivisorsSI = []prefixDivisor{
|
|
{"Y", 1e+24},
|
|
{"Z", 1e+21},
|
|
{"E", 1e+18},
|
|
{"P", 1e+15},
|
|
{"T", 1e+12},
|
|
{"G", 1e+9},
|
|
{"M", 1e+6},
|
|
{"k", 1e+3},
|
|
{"", 1e0},
|
|
{"m", 1e-3},
|
|
{"µ", 1e-6},
|
|
{"n", 1e-9},
|
|
{"p", 1e-12},
|
|
{"f", 1e-15},
|
|
{"a", 1e-18},
|
|
{"z", 1e-21},
|
|
{"y", 1e-24},
|
|
}
|
|
|
|
var prefixDivisorsIEC = []prefixDivisor{
|
|
{"Yi", 1 << 80},
|
|
{"Zi", 1 << 70},
|
|
{"Ei", 1 << 60},
|
|
{"Pi", 1 << 50},
|
|
{"Ti", 1 << 40},
|
|
{"Gi", 1 << 30},
|
|
{"Mi", 1 << 20},
|
|
{"Ki", 1 << 10},
|
|
{"", 1 << 0},
|
|
}
|
|
|
|
// appendFloat is similar to [strconv.AppendFloat] except it limits the output
|
|
// to a total number of digits rather than a fixed number of digits
|
|
// after the decimal point.
|
|
func appendFloat(b []byte, f float64, numDigits int) []byte {
|
|
i := len(b)
|
|
b = strconv.AppendFloat(b, f, 'f', numDigits, 64)
|
|
j := bytes.IndexByte(b[i:], '.')
|
|
if j >= numDigits {
|
|
return b[:i+j]
|
|
} else {
|
|
return b[:i+numDigits+len(".")]
|
|
}
|
|
}
|
|
|
|
func formatPrefixDivisor(f float64, numDigits int, unitSuffix string, prefixDivisors []prefixDivisor) string {
|
|
if math.IsNaN(f) || math.IsInf(f, 0) {
|
|
return strconv.FormatFloat(f, 'f', 0, 64)
|
|
}
|
|
var arr [16]byte
|
|
b := arr[:0]
|
|
if math.Signbit(f) {
|
|
f = math.Abs(f)
|
|
b = append(b, '-')
|
|
}
|
|
if f == 0 {
|
|
return string(append(append(b, '0'), unitSuffix...))
|
|
}
|
|
unit := strings.TrimLeftFunc(unitSuffix, unicode.IsSpace)
|
|
whitespace := unitSuffix[:len(unitSuffix)-len(unit)]
|
|
for i, pd := range prefixDivisors {
|
|
if f >= pd.divisor || i == len(prefixDivisors)-1 {
|
|
fdiv := f / pd.divisor
|
|
if float64(uint(fdiv))*pd.divisor == f {
|
|
b = strconv.AppendUint(b, uint64(fdiv), 10) // exact integer
|
|
} else {
|
|
b = appendFloat(b, fdiv, numDigits)
|
|
}
|
|
b = append(append(append(b, whitespace...), pd.prefix...), unit...)
|
|
break
|
|
}
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// FormatSI formats the number using a metric prefix (e.g. k, M, G, etc.)
|
|
// combined with the provided unit suffix. Any leading spaces in the unitSuffix
|
|
// are moved before the metric prefix.
|
|
//
|
|
// Example:
|
|
//
|
|
// FormatSI(123, "P/s") => "123P/s"
|
|
// FormatSI(12345678, " W") => "12.3 MW"
|
|
//
|
|
// Exact precision and width of the output may change in the future.
|
|
func FormatSI[N numeric](v N, unitSuffix string) string {
|
|
return formatPrefixDivisor(float64(v), len("999"), unitSuffix, prefixDivisorsSI)
|
|
}
|
|
|
|
// FormatIEC formats the number using a binary prefix (e.g. Ki, Mi, Gi, etc.)
|
|
// combined with the provided unit suffix. Any leading spaces in the unitSuffix
|
|
// are moved before the metric prefix.
|
|
//
|
|
// Example:
|
|
//
|
|
// FormatIEC(123, "B") => "123B"
|
|
// FormatIEC(12345678, " B/s") => "11.77 MiB/s"
|
|
//
|
|
// Exact precision and width of the output may change in the future.
|
|
func FormatIEC[N numeric](v N, unitSuffix string) string {
|
|
return formatPrefixDivisor(float64(v), len("1023"), unitSuffix, prefixDivisorsIEC)
|
|
}
|
|
|
|
// FormatDuration formats the duration with precision proportional to
|
|
// the magnitude of the duration. This differs from the [time.Duration.String]
|
|
// format in that it additionally supports year ('y') and day ('d') units,
|
|
// where a year is exactly 365.25 days and a day is exactly 24 hours.
|
|
//
|
|
// Examples:
|
|
//
|
|
// 12ns
|
|
// 5.12μs
|
|
// 832μs
|
|
// 8.14ms
|
|
// 512ms
|
|
// 1.3s
|
|
// 58s
|
|
// 5m23s
|
|
// 4h23m
|
|
// 4d16h
|
|
// 17y53d
|
|
func FormatDuration(d time.Duration) string {
|
|
const timeDay = 24 * time.Hour
|
|
const timeYear = 365*timeDay + timeDay/4
|
|
var b []byte
|
|
if d < 0 {
|
|
d = -1 * max(d, math.MinInt64+1) // avoid negation overflow
|
|
b = append(b, '-')
|
|
}
|
|
switch {
|
|
case d < 1e3*time.Nanosecond:
|
|
return string(append(b, d.Round(time.Nanosecond/1e0).String()...))
|
|
case d < 1e1*time.Microsecond:
|
|
return string(append(b, d.Round(time.Microsecond/1e2).String()...))
|
|
case d < 1e2*time.Microsecond:
|
|
return string(append(b, d.Round(time.Microsecond/1e1).String()...))
|
|
case d < 1e3*time.Microsecond:
|
|
return string(append(b, d.Round(time.Microsecond/1e0).String()...))
|
|
case d < 1e1*time.Millisecond:
|
|
return string(append(b, d.Round(time.Millisecond/1e2).String()...))
|
|
case d < 1e2*time.Millisecond:
|
|
return string(append(b, d.Round(time.Millisecond/1e1).String()...))
|
|
case d < 1e3*time.Millisecond:
|
|
return string(append(b, d.Round(time.Millisecond/1e0).String()...))
|
|
case d < 1e1*time.Second:
|
|
return string(append(b, d.Round(time.Second/1e1).String()...))
|
|
case d < time.Minute:
|
|
return string(append(b, d.Round(time.Second/1e0).String()...))
|
|
default:
|
|
// Invariant: d >= time.Minute at this point
|
|
units := []struct {
|
|
unit byte
|
|
period time.Duration
|
|
}{{'y', timeYear}, {'d', timeDay}, {'h', time.Hour}, {'m', time.Minute}, {'s', time.Second}}
|
|
for i, currUnit := range units {
|
|
if d >= currUnit.period {
|
|
nextUnit := units[i+1] // always safe since d >= time.Minute
|
|
if d%currUnit.period%nextUnit.period >= nextUnit.period/2 {
|
|
d = max(d, d+nextUnit.period/2) // round up in an overflow safe way
|
|
}
|
|
b = append(strconv.AppendUint(b, uint64(d/currUnit.period), 10), currUnit.unit)
|
|
d = d % currUnit.period
|
|
b = append(strconv.AppendUint(b, uint64(d/nextUnit.period), 10), nextUnit.unit)
|
|
return string(b)
|
|
}
|
|
}
|
|
panic("unreachable") // since d >= time.Minute
|
|
}
|
|
}
|