From c9fb8ca327af3b4a8ac063a1ed560087e62ef2bc Mon Sep 17 00:00:00 2001 From: Thomas Roehl Date: Thu, 10 Mar 2022 14:28:09 +0100 Subject: [PATCH] Unit system for ClusterCockpit to use similar names everywhere and ease unit conversions --- internal/ccUnits/ccUnitMeasure.go | 178 ++++++++++++++++++++++++++++++ internal/ccUnits/ccUnitScale.go | 131 ++++++++++++++++++++++ internal/ccUnits/ccUnits.go | 75 +++++++++++++ internal/ccUnits/ccUnits_test.go | 90 +++++++++++++++ 4 files changed, 474 insertions(+) create mode 100644 internal/ccUnits/ccUnitMeasure.go create mode 100644 internal/ccUnits/ccUnitScale.go create mode 100644 internal/ccUnits/ccUnits.go create mode 100644 internal/ccUnits/ccUnits_test.go diff --git a/internal/ccUnits/ccUnitMeasure.go b/internal/ccUnits/ccUnitMeasure.go new file mode 100644 index 0000000..640cdfc --- /dev/null +++ b/internal/ccUnits/ccUnitMeasure.go @@ -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 +} diff --git a/internal/ccUnits/ccUnitScale.go b/internal/ccUnits/ccUnitScale.go new file mode 100644 index 0000000..52b9689 --- /dev/null +++ b/internal/ccUnits/ccUnitScale.go @@ -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 "" + } +} + +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 + } +} diff --git a/internal/ccUnits/ccUnits.go b/internal/ccUnits/ccUnits.go new file mode 100644 index 0000000..6aaf36b --- /dev/null +++ b/internal/ccUnits/ccUnits.go @@ -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 +} diff --git a/internal/ccUnits/ccUnits_test.go b/internal/ccUnits/ccUnits_test.go new file mode 100644 index 0000000..2c9d654 --- /dev/null +++ b/internal/ccUnits/ccUnits_test.go @@ -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) + } + } +}