Unit system for ClusterCockpit to use similar names everywhere and ease unit conversions

This commit is contained in:
Thomas Roehl 2022-03-10 14:28:09 +01:00
parent 3cf2f69a07
commit c9fb8ca327
4 changed files with 474 additions and 0 deletions

View File

@ -0,0 +1,178 @@
package ccunits
import "regexp"
type Measure int
const (
None Measure = iota
Bytes
Flops
Percentage
TemperatureC
TemperatureF
Rotation
Hertz
Time
Power
Energy
Cycles
Requests
Packets
Events
)
func (m *Measure) String() string {
switch *m {
case Bytes:
return "Bytes"
case Flops:
return "Flops"
case Percentage:
return "Percent"
case TemperatureC:
return "DegreeC"
case TemperatureF:
return "DegreeF"
case Rotation:
return "RPM"
case Hertz:
return "Hertz"
case Time:
return "Seconds"
case Power:
return "Watts"
case Energy:
return "Joules"
case Cycles:
return "Cycles"
case Requests:
return "Requests"
case Packets:
return "Packets"
case Events:
return "Events"
default:
return "Unknown"
}
}
func (m *Measure) Short() string {
switch *m {
case Bytes:
return "Bytes"
case Flops:
return "Flops"
case Percentage:
return "Percent"
case TemperatureC:
return "degC"
case TemperatureF:
return "degF"
case Rotation:
return "RPM"
case Hertz:
return "Hz"
case Time:
return "s"
case Power:
return "W"
case Energy:
return "J"
case Cycles:
return "cyc"
case Requests:
return "requests"
case Packets:
return "packets"
case Events:
return "events"
default:
return "Unknown"
}
}
const bytesRegexStr = `^([bB][yY]?[tT]?[eE]?[sS]?)`
const flopsRegexStr = `^([fF][lL]?[oO]?[pP]?[sS]?)`
const percentRegexStr = `^(%%|[pP]ercent)`
const degreeCRegexStr = `^(deg[Cc]|°[cC])`
const degreeFRegexStr = `^(deg[fF]|°[fF])`
const rpmRegexStr = `^([rR][pP][mM])`
const hertzRegexStr = `^([hH][eE]?[rR]?[tT]?[zZ])`
const timeRegexStr = `^([sS][eE]?[cC]?[oO]?[nN]?[dD]?[sS]?)`
const powerRegexStr = `^([wW][aA]?[tT]?[tT]?[sS]?)`
const energyRegexStr = `^([jJ][oO]?[uU]?[lL]?[eE]?[sS]?)`
const cyclesRegexStr = `^([cC][yY][cC]?[lL]?[eE]?[sS]?)`
const requestsRegexStr = `^([rR][eE][qQ][uU]?[eE]?[sS]?[tT]?[sS]?)`
const packetsRegexStr = `^([pP][aA]?[cC]?[kK][eE]?[tT][sS]?)`
var bytesRegex = regexp.MustCompile(bytesRegexStr)
var flopsRegex = regexp.MustCompile(flopsRegexStr)
var percentRegex = regexp.MustCompile(percentRegexStr)
var degreeCRegex = regexp.MustCompile(degreeCRegexStr)
var degreeFRegex = regexp.MustCompile(degreeFRegexStr)
var rpmRegex = regexp.MustCompile(rpmRegexStr)
var hertzRegex = regexp.MustCompile(hertzRegexStr)
var timeRegex = regexp.MustCompile(timeRegexStr)
var powerRegex = regexp.MustCompile(powerRegexStr)
var energyRegex = regexp.MustCompile(energyRegexStr)
var cyclesRegex = regexp.MustCompile(cyclesRegexStr)
var requestsRegex = regexp.MustCompile(requestsRegexStr)
var packetsRegex = regexp.MustCompile(packetsRegexStr)
func NewMeasure(unit string) Measure {
var match []string
match = bytesRegex.FindStringSubmatch(unit)
if match != nil {
return Bytes
}
match = flopsRegex.FindStringSubmatch(unit)
if match != nil {
return Flops
}
match = percentRegex.FindStringSubmatch(unit)
if match != nil {
return Percentage
}
match = degreeCRegex.FindStringSubmatch(unit)
if match != nil {
return TemperatureC
}
match = degreeFRegex.FindStringSubmatch(unit)
if match != nil {
return TemperatureF
}
match = rpmRegex.FindStringSubmatch(unit)
if match != nil {
return Rotation
}
match = hertzRegex.FindStringSubmatch(unit)
if match != nil {
return Hertz
}
match = timeRegex.FindStringSubmatch(unit)
if match != nil {
return Time
}
match = cyclesRegex.FindStringSubmatch(unit)
if match != nil {
return Cycles
}
match = powerRegex.FindStringSubmatch(unit)
if match != nil {
return Power
}
match = energyRegex.FindStringSubmatch(unit)
if match != nil {
return Energy
}
match = requestsRegex.FindStringSubmatch(unit)
if match != nil {
return Requests
}
match = packetsRegex.FindStringSubmatch(unit)
if match != nil {
return Packets
}
return None
}

View File

@ -0,0 +1,131 @@
package ccunits
import "regexp"
type Scale float64
const (
Base Scale = iota
Peta = 1e15
Tera = 1e12
Giga = 1e9
Mega = 1e6
Kilo = 1e3
Milli = 1e-3
Micro = 1e-6
Nano = 1e-9
Kibi = 1024
Mebi = 1024 * 1024
Gibi = 1024 * 1024 * 1024
Tebi = 1024 * 1024 * 1024 * 1024
)
const prefixRegexStr = `^([kKmMgGtTpP]?[i]?)(.*)`
var prefixRegex = regexp.MustCompile(prefixRegexStr)
func (s *Scale) String() string {
switch *s {
case Base:
return ""
case Kilo:
return "Kilo"
case Mega:
return "Mega"
case Giga:
return "Giga"
case Tera:
return "Tera"
case Peta:
return "Peta"
case Milli:
return "Milli"
case Micro:
return "Micro"
case Nano:
return "Nano"
case Kibi:
return "Kibi"
case Mebi:
return "Mebi"
case Gibi:
return "Gibi"
case Tebi:
return "Tebi"
default:
return "Unkn"
}
}
func (s *Scale) Prefix() string {
switch *s {
case Base:
return ""
case Kilo:
return "K"
case Mega:
return "M"
case Giga:
return "G"
case Tera:
return "T"
case Peta:
return "P"
case Milli:
return "m"
case Micro:
return "u"
case Nano:
return "n"
case Kibi:
return "Ki"
case Mebi:
return "Mi"
case Gibi:
return "Gi"
case Tebi:
return "Ti"
default:
return "<unkn>"
}
}
func NewScale(prefix string) Scale {
switch prefix {
case "k":
return Kilo
case "K":
return Kilo
case "m":
return Milli
case "M":
return Mega
case "g":
return Giga
case "G":
return Giga
case "t":
return Tera
case "T":
return Tera
case "u":
return Micro
case "n":
return Nano
case "ki":
return Kibi
case "Ki":
return Kibi
case "Mi":
return Mebi
case "gi":
return Gibi
case "Gi":
return Gibi
case "Ti":
return Tebi
case "":
return Base
default:
return Base
}
}

View File

@ -0,0 +1,75 @@
package ccunits
import (
"fmt"
"strings"
)
type Unit struct {
scale Scale
measure Measure
divMeasure Measure
}
func (u *Unit) String() string {
if u.divMeasure != None {
return fmt.Sprintf("%s%s/%s", u.scale.String(), u.measure.String(), u.divMeasure.String())
} else {
return fmt.Sprintf("%s%s", u.scale.String(), u.measure.String())
}
}
func (u *Unit) Short() string {
if u.divMeasure != None {
return fmt.Sprintf("%s%s/%s", u.scale.Prefix(), u.measure.Short(), u.divMeasure.Short())
} else {
return fmt.Sprintf("%s%s", u.scale.Prefix(), u.measure.Short())
}
}
func (u *Unit) AddDivisorUnit(div Measure) {
u.divMeasure = div
}
func GetScaleFactor(in Scale, out Scale) float64 {
var factor = 1.0
var in_scale = 1.0
var out_scale = 1.0
if in != Base {
in_scale = float64(in)
}
if out != Base {
out_scale = float64(out)
}
factor = in_scale / out_scale
return factor
}
func GetUnitScaleFactor(in Unit, out Unit) (float64, error) {
if in.measure != out.measure || in.divMeasure != out.divMeasure {
return 1.0, fmt.Errorf("invalid measures in in and out Unit")
}
return GetScaleFactor(in.scale, out.scale), nil
}
func NewUnit(unit string) Unit {
u := Unit{
scale: Base,
measure: None,
divMeasure: None,
}
matches := prefixRegex.FindStringSubmatch(unit)
if len(matches) > 2 {
u.scale = NewScale(matches[1])
measures := strings.Split(matches[2], "/")
u.measure = NewMeasure(measures[0])
// Special case for 'm' as scale for Bytes as thers is nothing like MilliBytes
if u.measure == Bytes && u.scale == Milli {
u.scale = Mega
}
if len(measures) > 1 {
u.divMeasure = NewMeasure(measures[1])
}
}
return u
}

View File

@ -0,0 +1,90 @@
package ccunits
import (
"fmt"
"testing"
)
func TestUnitsExact(t *testing.T) {
testCases := []struct {
in string
want Unit
}{
{"b", NewUnit("Bytes")},
{"B", NewUnit("Bytes")},
{"byte", NewUnit("Bytes")},
{"bytes", NewUnit("Bytes")},
{"BYtes", NewUnit("Bytes")},
{"Mb", NewUnit("MBytes")},
{"MB", NewUnit("MBytes")},
{"Mbyte", NewUnit("MBytes")},
{"Mbytes", NewUnit("MBytes")},
{"MbYtes", NewUnit("MBytes")},
{"Gb", NewUnit("GBytes")},
{"GB", NewUnit("GBytes")},
{"Hz", NewUnit("Hertz")},
{"MHz", NewUnit("MHertz")},
{"GHertz", NewUnit("GHertz")},
{"pkts", NewUnit("Packets")},
{"packets", NewUnit("Packets")},
{"packet", NewUnit("Packets")},
{"flop", NewUnit("Flops")},
{"flops", NewUnit("Flops")},
{"floPS", NewUnit("Flops")},
{"Mflop", NewUnit("MFlops")},
{"Gflop", NewUnit("GFlops")},
{"gflop", NewUnit("GFlops")},
{"%", NewUnit("Percent")},
{"percent", NewUnit("Percent")},
{"degc", NewUnit("degC")},
{"degC", NewUnit("degC")},
{"degf", NewUnit("degF")},
{"°f", NewUnit("degF")},
}
compareUnitExact := func(in, out Unit) bool {
if in.measure == out.measure && in.divMeasure == out.divMeasure && in.scale == out.scale {
return true
}
return false
}
for _, c := range testCases {
u := NewUnit(c.in)
if !compareUnitExact(u, c.want) {
t.Errorf("func NewUnit(%q) == %q, want %q", c.in, u.String(), c.want.String())
}
}
}
func TestUnitsDifferentScale(t *testing.T) {
testCases := []struct {
in string
want Unit
scaleFactor float64
}{
{"kb", NewUnit("Bytes"), 1000},
{"Mb", NewUnit("Bytes"), 1000000},
{"Mb/s", NewUnit("Bytes/s"), 1000000},
{"Flops/s", NewUnit("MFlops/s"), 1e-6},
{"Flops/s", NewUnit("GFlops/s"), 1e-9},
{"MHz", NewUnit("Hertz"), 1e6},
{"kb", NewUnit("Kib"), 1000.0 / 1024},
{"Mib", NewUnit("MBytes"), (1024 * 1024.0) / (1e6)},
{"mb", NewUnit("MBytes"), 1.0},
}
compareUnitWithScale := func(in, out Unit, factor float64) bool {
if in.measure == out.measure && in.divMeasure == out.divMeasure {
if f := GetScaleFactor(in.scale, out.scale); f == factor {
return true
} else {
fmt.Println(f)
}
}
return false
}
for _, c := range testCases {
u := NewUnit(c.in)
if !compareUnitWithScale(u, c.want, c.scaleFactor) {
t.Errorf("func NewUnit(%q) == %q, want %q with factor %f", c.in, u.String(), c.want.String(), c.scaleFactor)
}
}
}