From a6e40789e4fc93e99bfa4680eee9cec0a3035555 Mon Sep 17 00:00:00 2001 From: Thomas Roehl Date: Thu, 17 Mar 2022 14:18:07 +0100 Subject: [PATCH] Add new prefixes, add some more interface functions --- README.md | 31 +++++++++++++++++--- ccUnitMeasure.go | 12 +++++--- ccUnitPrefix.go | 57 ++++++++++++++++++++++++++---------- ccUnits.go | 67 ++++++++++++++++++++++++++++++++---------- ccUnits_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 200 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 41e89bd..2335a3f 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ When working with metrics, the problem comes up that they may use different unit In order to enable unit comparison and conversion, the ccUnits package provides some helpers: There are basically two important functions: ```go -NewUnit(unit string) Unit -GetUnitPrefixFactor(in Unit, out Unit) (func(value float64) float64, error) // Get conversion function for the value +NewUnit(unit string) Unit // create a new unit from some string like 'GHz', 'Mbyte' or 'kevents/s' +func GetUnitUnitFactor(in Unit, out Unit) (func(value float64) float64, error) // Get conversion function between two units +func GetPrefixFactor(in Prefix, out Prefix) func(value float64) float64 // Get conversion function between two prefixes +func GetUnitPrefixFactor(in Unit, out Prefix) (func(value float64) float64, Unit) // Get conversion function for prefix changes and the new unit for further use type Unit interface { Valid() bool @@ -26,14 +28,35 @@ v := NewUnit("foo") fmt.Println(v.Valid()) // false ``` -If you have two units and need the conversion function: +If you have two units or other components and need the conversion function: ```go +// Get conversion functions for 'kB' to 'MBytes' u1 := NewUnit("kB") u2 := NewUnit("MBytes") -convFunc, err := GetUnitPrefixFactor(u1, u2) // Returns an error if the units have different measures +convFunc, err := GetUnitUnitFactor(u1, u2) // Returns an error if the units have different measures if err == nil { v2 := convFunc(v1) + fmt.Printf("%f %s\n", v2, u2.Short()) } +// Get conversion function for 'kB' -> 'G' prefix. +// Returns the function and the new unit 'GBytes' +p1 := NewPrefix("G") +convFunc, u_p1 := GetUnitPrefixFactor(u1, p1) +// or +// convFunc, u_p1 := GetUnitPrefixStringFactor(u1, "G") +if convFunc != nil { + v2 := convFunc(v1) + fmt.Printf("%f %s\n", v2, u_p1.Short()) +} +// Get conversion function for two prefixes: 'G' -> 'T' +p2 := NewPrefix("T") +convFunc = GetPrefixPrefixFactor(p1, p2) +if convFunc != nil { + v2 := convFunc(v1) + fmt.Printf("%f %s -> %f %s\n", v1, p1.Prefix(), v2, p2.Prefix()) +} + + ``` (In the ClusterCockpit ecosystem the separation between values and units if useful since they are commonly not stored as a single entity but the value is a field in the CCMetric while unit is a tag or a meta information). diff --git a/ccUnitMeasure.go b/ccUnitMeasure.go index de8f7a0..d098e1c 100644 --- a/ccUnitMeasure.go +++ b/ccUnitMeasure.go @@ -5,7 +5,7 @@ import "regexp" type Measure int const ( - None Measure = iota + InvalidMeasure Measure = iota Bytes Flops Percentage @@ -25,7 +25,7 @@ const ( func (m *Measure) String() string { switch *m { case Bytes: - return "Bytes" + return "byte" case Flops: return "Flops" case Percentage: @@ -52,6 +52,8 @@ func (m *Measure) String() string { return "Packets" case Events: return "Events" + case InvalidMeasure: + return "Invalid" default: return "Unknown" } @@ -60,7 +62,7 @@ func (m *Measure) String() string { func (m *Measure) Short() string { switch *m { case Bytes: - return "Bytes" + return "B" case Flops: return "Flops" case Percentage: @@ -87,6 +89,8 @@ func (m *Measure) Short() string { return "packets" case Events: return "events" + case InvalidMeasure: + return "Invalid" default: return "Unknown" } @@ -180,5 +184,5 @@ func NewMeasure(unit string) Measure { if match != nil { return Events } - return None + return InvalidMeasure } diff --git a/ccUnitPrefix.go b/ccUnitPrefix.go index ae3fe0d..e90f594 100644 --- a/ccUnitPrefix.go +++ b/ccUnitPrefix.go @@ -5,20 +5,27 @@ import "regexp" type Prefix float64 const ( - Base Prefix = 1 - Exa = 1e18 - 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 + InvalidPrefix Prefix = iota + Base = 1 + Yotta = 1e24 + Zetta = 1e21 + Exa = 1e18 + 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 + Pebi = 1024 * 1024 * 1024 * 1024 * 1024 + Exbi = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + Zebi = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + Yobi = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 ) const prefixRegexStr = `^([kKmMgGtTpP]?[i]?)(.*)` @@ -26,6 +33,8 @@ var prefixRegex = regexp.MustCompile(prefixRegexStr) func (s *Prefix) String() string { switch *s { + case InvalidPrefix: + return "Inval" case Base: return "" case Kilo: @@ -40,6 +49,10 @@ func (s *Prefix) String() string { return "Peta" case Exa: return "Exa" + case Zetta: + return "Zetta" + case Yotta: + return "Yotta" case Milli: return "Milli" case Micro: @@ -61,6 +74,8 @@ func (s *Prefix) String() string { func (s *Prefix) Prefix() string { switch *s { + case InvalidPrefix: + return "" case Base: return "" case Kilo: @@ -75,6 +90,10 @@ func (s *Prefix) Prefix() string { return "P" case Exa: return "E" + case Zetta: + return "Z" + case Yotta: + return "Y" case Milli: return "m" case Micro: @@ -120,6 +139,14 @@ func NewPrefix(prefix string) Prefix { return Exa case "E": return Exa + case "z": + return Zetta + case "Z": + return Zetta + case "y": + return Yotta + case "Y": + return Yotta case "u": return Micro case "n": @@ -139,6 +166,6 @@ func NewPrefix(prefix string) Prefix { case "": return Base default: - return Base + return InvalidPrefix } } diff --git a/ccUnits.go b/ccUnits.go index b339ef3..fb6804c 100644 --- a/ccUnits.go +++ b/ccUnits.go @@ -19,14 +19,17 @@ type Unit interface { getPrefix() Prefix getMeasure() Measure getDivMeasure() Measure + setPrefix(p Prefix) } +var INVALID_UNIT = NewUnit("foobar") + func (u *unit) Valid() bool { - return u.measure != None + return u.measure != InvalidMeasure } func (u *unit) String() string { - if u.divMeasure != None { + if u.divMeasure != InvalidMeasure { return fmt.Sprintf("%s%s/%s", u.prefix.String(), u.measure.String(), u.divMeasure.String()) } else { return fmt.Sprintf("%s%s", u.prefix.String(), u.measure.String()) @@ -34,7 +37,7 @@ func (u *unit) String() string { } func (u *unit) Short() string { - if u.divMeasure != None { + if u.divMeasure != InvalidMeasure { return fmt.Sprintf("%s%s/%s", u.prefix.Prefix(), u.measure.Short(), u.divMeasure.Short()) } else { return fmt.Sprintf("%s%s", u.prefix.Prefix(), u.measure.Short()) @@ -49,6 +52,10 @@ func (u *unit) getPrefix() Prefix { return u.prefix } +func (u *unit) setPrefix(p Prefix) { + u.prefix = p +} + func (u *unit) getMeasure() Measure { return u.measure } @@ -57,7 +64,7 @@ func (u *unit) getDivMeasure() Measure { return u.divMeasure } -func GetPrefixFactor(in Prefix, out Prefix) func(value float64) float64 { +func GetPrefixPrefixFactor(in Prefix, out Prefix) func(value float64) float64 { var factor = 1.0 var in_prefix = float64(in) var out_prefix = float64(out) @@ -65,7 +72,33 @@ func GetPrefixFactor(in Prefix, out Prefix) func(value float64) float64 { return func(value float64) float64 { return factor } } -func GetUnitPrefixFactor(in Unit, out Unit) (func(value float64) float64, error) { +func GetPrefixStringPrefixStringFactor(in string, out string) func(value float64) float64 { + var i Prefix = NewPrefix(in) + var o Prefix = NewPrefix(out) + return GetPrefixPrefixFactor(i, o) +} + +func GetUnitPrefixFactor(in Unit, out Prefix) (func(value float64) float64, Unit) { + outUnit := NewUnit(in.Short()) + if outUnit.Valid() { + outUnit.setPrefix(out) + conv := GetPrefixPrefixFactor(in.getPrefix(), out) + return conv, outUnit + } + return nil, INVALID_UNIT +} + +func GetUnitPrefixStringFactor(in Unit, out string) (func(value float64) float64, Unit) { + var o Prefix = NewPrefix(out) + return GetUnitPrefixFactor(in, o) +} + +func GetUnitStringPrefixStringFactor(in string, out string) (func(value float64) float64, Unit) { + var i = NewUnit(in) + return GetUnitPrefixStringFactor(i, out) +} + +func GetUnitUnitFactor(in Unit, out Unit) (func(value float64) float64, error) { if in.getMeasure() == TemperatureC && out.getMeasure() == TemperatureF { return func(value float64) float64 { return (value * 1.8) + 32 }, nil } else if in.getMeasure() == TemperatureF && out.getMeasure() == TemperatureC { @@ -73,14 +106,14 @@ func GetUnitPrefixFactor(in Unit, out Unit) (func(value float64) float64, error) } else if in.getMeasure() != out.getMeasure() || in.getDivMeasure() != out.getDivMeasure() { return func(value float64) float64 { return 1.0 }, fmt.Errorf("invalid measures in in and out Unit") } - return GetPrefixFactor(in.getPrefix(), out.getPrefix()), nil + return GetPrefixPrefixFactor(in.getPrefix(), out.getPrefix()), nil } func NewUnit(unitStr string) Unit { u := &unit{ - prefix: Base, - measure: None, - divMeasure: None, + prefix: InvalidPrefix, + measure: InvalidMeasure, + divMeasure: InvalidMeasure, } matches := prefixRegex.FindStringSubmatch(unitStr) if len(matches) > 2 { @@ -90,17 +123,17 @@ func NewUnit(unitStr string) Unit { // Special case for prefix 'p' or 'P' (Peta) and measures starting with 'p' or 'P' // like 'packets' or 'percent'. Same for 'e' or 'E' (Exa) for measures starting with // 'e' or 'E' like 'events' - if m == None { + if m == InvalidMeasure { switch pre { case Peta, Exa: t := NewMeasure(matches[1] + measures[0]) - if t != None { + if t != InvalidMeasure { m = t pre = Base } } } - div := None + div := InvalidMeasure if len(measures) > 1 { div = NewMeasure(measures[1]) } @@ -115,9 +148,13 @@ func NewUnit(unitStr string) Unit { case Percentage: pre = Base } - u.prefix = pre - u.measure = m - u.divMeasure = div + if pre != InvalidPrefix && m != InvalidMeasure { + u.prefix = pre + u.measure = m + if div != InvalidMeasure { + u.divMeasure = div + } + } } return u } diff --git a/ccUnits_test.go b/ccUnits_test.go index 40ee967..7a115c7 100644 --- a/ccUnits_test.go +++ b/ccUnits_test.go @@ -24,7 +24,7 @@ func TestUnitsExact(t *testing.T) { {"GB", NewUnit("GBytes")}, {"Hz", NewUnit("Hertz")}, {"MHz", NewUnit("MHertz")}, - {"GHertz", NewUnit("GHertz")}, + {"GHz", NewUnit("GHertz")}, {"pkts", NewUnit("Packets")}, {"packets", NewUnit("Packets")}, {"packet", NewUnit("Packets")}, @@ -44,7 +44,7 @@ func TestUnitsExact(t *testing.T) { {"event", NewUnit("events")}, {"EveNts", NewUnit("events")}, {"reqs", NewUnit("requests")}, - {"requests", NewUnit("requests")}, + {"reQuEsTs", NewUnit("requests")}, {"Requests", NewUnit("requests")}, {"cyc", NewUnit("cycles")}, {"cy", NewUnit("cycles")}, @@ -79,7 +79,7 @@ func TestUnitsExact(t *testing.T) { } } -func TestUnitsDifferentPrefix(t *testing.T) { +func TestUnitUnitConversion(t *testing.T) { testCases := []struct { in string want Unit @@ -97,7 +97,7 @@ func TestUnitsDifferentPrefix(t *testing.T) { } compareUnitWithPrefix := func(in, out Unit, factor float64) bool { if in.getMeasure() == out.getMeasure() && in.getDivMeasure() == out.getDivMeasure() { - if f := GetPrefixFactor(in.getPrefix(), out.getPrefix()); f(1.0) == factor { + if f := GetPrefixPrefixFactor(in.getPrefix(), out.getPrefix()); f(1.0) == factor { return true } else { fmt.Println(f(1.0)) @@ -108,7 +108,73 @@ func TestUnitsDifferentPrefix(t *testing.T) { for _, c := range testCases { u := NewUnit(c.in) if (!u.Valid()) || (!compareUnitWithPrefix(u, c.want, c.prefixFactor)) { - t.Errorf("func NewUnit(%q) == %q, want %q with factor %f", c.in, u.String(), c.want.String(), c.prefixFactor) + t.Errorf("GetPrefixPrefixFactor(%q, %q) invalid, want %q with factor %f", c.in, u.String(), c.want.String(), c.prefixFactor) + } + } +} + +func TestUnitPrefixConversion(t *testing.T) { + testCases := []struct { + in string + want string + prefixFactor float64 + wantUnit Unit + }{ + {"KBytes", "", 1000, NewUnit("Bytes")}, + {"MBytes", "", 1e6, NewUnit("Bytes")}, + {"MBytes", "G", 1e-3, NewUnit("GBytes")}, + {"mb", "M", 1, NewUnit("MBytes")}, + } + compareUnitPrefix := func(in Unit, out Prefix, factor float64, outUnit Unit) bool { + if in.Valid() { + conv, unit := GetUnitPrefixFactor(in, out) + value := conv(1.0) + if value == factor && unit.String() == outUnit.String() { + return true + } + } + return false + } + for _, c := range testCases { + u := NewUnit(c.in) + p := NewPrefix(c.want) + if (!u.Valid()) || (!compareUnitPrefix(u, p, c.prefixFactor, c.wantUnit)) { + t.Errorf("GetUnitPrefixFactor(%q, %q) invalid, want %q with factor %f", c.in, p.Prefix(), c.wantUnit.String(), c.prefixFactor) + } + + } +} + +func TestPrefixPrefixConversion(t *testing.T) { + testCases := []struct { + in string + want string + prefixFactor float64 + }{ + {"K", "", 1000}, + {"M", "", 1e6}, + {"M", "G", 1e-3}, + {"", "M", 1e-6}, + {"", "m", 1e3}, + {"m", "n", 1e6}, + //{"", "n", 1e9} does not work because of IEEE rounding problems + } + comparePrefixPrefix := func(in Prefix, out Prefix, factor float64) bool { + if in != InvalidPrefix && out != InvalidPrefix { + conv := GetPrefixPrefixFactor(in, out) + value := conv(1.0) + fmt.Println("1.0 -> ", value, ", want ", factor) + if value == factor { + return true + } + } + return false + } + for _, c := range testCases { + i := NewPrefix(c.in) + o := NewPrefix(c.want) + if !comparePrefixPrefix(i, o, c.prefixFactor) { + t.Errorf("GetPrefixPrefixFactor(%q, %q) invalid, want %q with factor %f", c.in, c.want, o.Prefix(), c.prefixFactor) } } }