Use maps to store all unit related data

This commit is contained in:
Thomas Roehl 2022-06-09 14:50:02 +02:00
parent ddf955ff23
commit ddcfc07745
4 changed files with 231 additions and 289 deletions

View File

@ -1,9 +1,8 @@
# ccUnits - A unit system for ClusterCockpit # cc-units - A unit system for ClusterCockpit
When working with metrics, the problem comes up that they may use different unit name but have the same unit in fact. There are a lot of real world examples like 'kB' and 'Kbyte'. In CC Metric Collector, the Collectors read data from different sources which may use different units or the programmer specifies a unit for a metric by hand. The ccUnits system is not comparable with the SI unit system. If you are looking for a package for the SI units, see [here](https://pkg.go.dev/github.com/gurre/si). When working with metrics, the problem comes up that they may use different unit name but have the same unit in fact. There are a lot of real world examples like 'kB' and 'Kbyte'. In [cc-metric-collector](https://github.com/ClusterCockpit/cc-metric-collector), the collectors read data from different sources which may use different units or the programmer specifies a unit for a metric by hand. The cc-units system is not comparable with the SI unit system. If you are looking for a package for the SI units, see [here](https://pkg.go.dev/github.com/gurre/si).
In order to enable unit comparison and conversion, the ccUnits package provides some helpers: In order to enable unit comparison and conversion, the ccUnits package provides some helpers:
There are basically two important functions:
```go ```go
NewUnit(unit string) Unit // create a new unit from some string like 'GHz', 'Mbyte' or 'kevents/s' 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 GetUnitUnitFactor(in Unit, out Unit) (func(value float64) float64, error) // Get conversion function between two units

View File

@ -12,7 +12,7 @@ const (
TemperatureC TemperatureC
TemperatureF TemperatureF
Rotation Rotation
Hertz Frequency
Time Time
Watt Watt
Joule Joule
@ -22,171 +22,113 @@ const (
Events Events
) )
type MeasureData struct {
Long string
Short string
Regex string
}
// Different names and regex used for input and output
var InvalidMeasureLong string = "Invalid"
var InvalidMeasureShort string = "inval"
var MeasuresMap map[Measure]MeasureData = map[Measure]MeasureData{
Bytes: {
Long: "byte",
Short: "B",
Regex: "^([bB][yY]?[tT]?[eE]?[sS]?)",
},
Flops: {
Long: "Flops",
Short: "Flops",
Regex: "^([fF][lL]?[oO]?[pP]?[sS]?)",
},
Percentage: {
Long: "Percent",
Short: "%",
Regex: "^(%|[pP]ercent)",
},
TemperatureC: {
Long: "DegreeC",
Short: "degC",
Regex: "^(deg[Cc]|°[cC])",
},
TemperatureF: {
Long: "DegreeF",
Short: "degF",
Regex: "^(deg[fF]|°[fF])",
},
Rotation: {
Long: "RPM",
Short: "RPM",
Regex: "^([rR][pP][mM])",
},
Frequency: {
Long: "Hertz",
Short: "Hz",
Regex: "^([hH][eE]?[rR]?[tT]?[zZ])",
},
Time: {
Long: "Seconds",
Short: "s",
Regex: "^([sS][eE]?[cC]?[oO]?[nN]?[dD]?[sS]?)",
},
Cycles: {
Long: "Cycles",
Short: "cyc",
Regex: "^([cC][yY][cC]?[lL]?[eE]?[sS]?)",
},
Watt: {
Long: "Watts",
Short: "W",
Regex: "^([wW][aA]?[tT]?[tT]?[sS]?)",
},
Joule: {
Long: "Joules",
Short: "J",
Regex: "^([jJ][oO]?[uU]?[lL]?[eE]?[sS]?)",
},
Requests: {
Long: "Requests",
Short: "requests",
Regex: "^([rR][eE][qQ][uU]?[eE]?[sS]?[tT]?[sS]?)",
},
Packets: {
Long: "Packets",
Short: "packets",
Regex: "^([pP][aA]?[cC]?[kK][eE]?[tT][sS]?)",
},
Events: {
Long: "Events",
Short: "events",
Regex: "^([eE][vV]?[eE]?[nN][tT][sS]?)",
},
}
// String returns the long string for the measure like 'Percent' or 'Seconds' // String returns the long string for the measure like 'Percent' or 'Seconds'
func (m *Measure) String() string { func (m *Measure) String() string {
switch *m { if data, ok := MeasuresMap[*m]; ok {
case Bytes: return data.Long
return "byte"
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 Watt:
return "Watts"
case Joule:
return "Joules"
case Cycles:
return "Cycles"
case Requests:
return "Requests"
case Packets:
return "Packets"
case Events:
return "Events"
case InvalidMeasure:
return "Invalid"
default:
return "Unknown"
} }
return InvalidMeasureLong
} }
// Short returns the short string for the measure like 'B' (Bytes), 's' (Time) or 'W' (Watt). Is is recommened to use Short() over String(). // Short returns the short string for the measure like 'B' (Bytes), 's' (Time) or 'W' (Watt). Is is recommened to use Short() over String().
func (m *Measure) Short() string { func (m *Measure) Short() string {
switch *m { if data, ok := MeasuresMap[*m]; ok {
case Bytes: return data.Short
return "B"
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 Watt:
return "W"
case Joule:
return "J"
case Cycles:
return "cyc"
case Requests:
return "requests"
case Packets:
return "packets"
case Events:
return "events"
case InvalidMeasure:
return "Invalid"
default:
return "Unknown"
} }
return InvalidMeasureShort
} }
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 wattRegexStr = `^([wW][aA]?[tT]?[tT]?[sS]?)`
const jouleRegexStr = `^([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]?)`
const eventsRegexStr = `^([eE][vV]?[eE]?[nN][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 wattRegex = regexp.MustCompile(wattRegexStr)
var jouleRegex = regexp.MustCompile(jouleRegexStr)
var cyclesRegex = regexp.MustCompile(cyclesRegexStr)
var requestsRegex = regexp.MustCompile(requestsRegexStr)
var packetsRegex = regexp.MustCompile(packetsRegexStr)
var eventsRegex = regexp.MustCompile(eventsRegexStr)
// NewMeasure creates a new measure out of a string representing a measure like 'Bytes', 'Flops' and 'precent'. // NewMeasure creates a new measure out of a string representing a measure like 'Bytes', 'Flops' and 'precent'.
// It uses regular expressions for matching. // It uses regular expressions for matching.
func NewMeasure(unit string) Measure { func NewMeasure(unit string) Measure {
var match []string for m, data := range MeasuresMap {
match = bytesRegex.FindStringSubmatch(unit) regex := regexp.MustCompile(data.Regex)
match := regex.FindStringSubmatch(unit)
if match != nil { if match != nil {
return Bytes return m
} }
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 = wattRegex.FindStringSubmatch(unit)
if match != nil {
return Watt
}
match = jouleRegex.FindStringSubmatch(unit)
if match != nil {
return Joule
}
match = requestsRegex.FindStringSubmatch(unit)
if match != nil {
return Requests
}
match = packetsRegex.FindStringSubmatch(unit)
if match != nil {
return Packets
}
match = eventsRegex.FindStringSubmatch(unit)
if match != nil {
return Events
} }
return InvalidMeasure return InvalidMeasure
} }

View File

@ -1,6 +1,8 @@
package ccunits package ccunits
import "regexp" import (
"regexp"
)
type Prefix float64 type Prefix float64
@ -27,148 +29,146 @@ const (
Zebi = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 Zebi = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024
Yobi = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 Yobi = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024
) )
const prefixRegexStr = `^([kKmMgGtTpP]?[i]?)(.*)` const PrefixUnitSplitRegexStr = `^([kKmMgGtTpPeEzZyY]?[i]?)(.*)`
var prefixRegex = regexp.MustCompile(prefixRegexStr) var prefixUnitSplitRegex = regexp.MustCompile(PrefixUnitSplitRegexStr)
type PrefixData struct {
Long string
Short string
Regex string
}
// Different names and regex used for input and output
var InvalidPrefixLong string = "Invalid"
var InvalidPrefixShort string = "inval"
var PrefixDataMap map[Prefix]PrefixData = map[Prefix]PrefixData{
Base: {
Long: "",
Short: "",
Regex: "^$",
},
Kilo: {
Long: "Kilo",
Short: "K",
Regex: "^[kK]$",
},
Mega: {
Long: "Mega",
Short: "M",
Regex: "^[M]$",
},
Giga: {
Long: "Giga",
Short: "G",
Regex: "^[gG]$",
},
Tera: {
Long: "Tera",
Short: "T",
Regex: "^[tT]$",
},
Peta: {
Long: "Peta",
Short: "P",
Regex: "^[pP]$",
},
Exa: {
Long: "Exa",
Short: "E",
Regex: "^[eE]$",
},
Zetta: {
Long: "Zetta",
Short: "Z",
Regex: "^[zZ]$",
},
Yotta: {
Long: "Yotta",
Short: "Y",
Regex: "^[yY]$",
},
Milli: {
Long: "Milli",
Short: "m",
Regex: "^[m]$",
},
Micro: {
Long: "Micro",
Short: "u",
Regex: "^[u]$",
},
Nano: {
Long: "Nano",
Short: "n",
Regex: "^[n]$",
},
Kibi: {
Long: "Kibi",
Short: "Ki",
Regex: "^[kK][i]$",
},
Mebi: {
Long: "Mebi",
Short: "Mi",
Regex: "^[M][i]$",
},
Gibi: {
Long: "Gibi",
Short: "Gi",
Regex: "^[gG][i]$",
},
Tebi: {
Long: "Tebi",
Short: "Ti",
Regex: "^[tT][i]$",
},
Pebi: {
Long: "Pebi",
Short: "Pi",
Regex: "^[pP][i]$",
},
Exbi: {
Long: "Exbi",
Short: "Ei",
Regex: "^[eE][i]$",
},
Zebi: {
Long: "Zebi",
Short: "Zi",
Regex: "^[zZ][i]$",
},
Yobi: {
Long: "Yobi",
Short: "Yi",
Regex: "^[yY][i]$",
},
}
// String returns the long string for the prefix like 'Kilo' or 'Mega' // String returns the long string for the prefix like 'Kilo' or 'Mega'
func (s *Prefix) String() string { func (p *Prefix) String() string {
switch *s { if data, ok := PrefixDataMap[*p]; ok {
case InvalidPrefix: return data.Long
return "Inval"
case Base:
return ""
case Kilo:
return "Kilo"
case Mega:
return "Mega"
case Giga:
return "Giga"
case Tera:
return "Tera"
case Peta:
return "Peta"
case Exa:
return "Exa"
case Zetta:
return "Zetta"
case Yotta:
return "Yotta"
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"
} }
return InvalidMeasureLong
} }
// Prefix returns the short string for the prefix like 'K', 'M' or 'G'. Is is recommened to use Prefix() over String(). // Prefix returns the short string for the prefix like 'K', 'M' or 'G'. Is is recommened to use Prefix() over String().
func (s *Prefix) Prefix() string { func (p *Prefix) Prefix() string {
switch *s { if data, ok := PrefixDataMap[*p]; ok {
case InvalidPrefix: return data.Short
return "<inval>"
case Base:
return ""
case Kilo:
return "K"
case Mega:
return "M"
case Giga:
return "G"
case Tera:
return "T"
case Peta:
return "P"
case Exa:
return "E"
case Zetta:
return "Z"
case Yotta:
return "Y"
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>"
} }
return InvalidMeasureShort
} }
// NewPrefix creates a new prefix out of a string representing a unit like 'k', 'K', 'M' or 'G'. // NewPrefix creates a new prefix out of a string representing a unit like 'k', 'K', 'M' or 'G'.
func NewPrefix(prefix string) Prefix { func NewPrefix(prefix string) Prefix {
switch prefix { for p, data := range PrefixDataMap {
case "k": regex := regexp.MustCompile(data.Regex)
return Kilo match := regex.FindStringSubmatch(prefix)
case "K": if match != nil {
return Kilo return p
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 "p":
return Peta
case "P":
return Peta
case "e":
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":
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 InvalidPrefix return InvalidPrefix
} }
}

View File

@ -1,3 +1,4 @@
// Unit system for cluster monitoring metrics like bytes, flops and events
package ccunits package ccunits
import ( import (
@ -204,7 +205,7 @@ func NewUnit(unitStr string) Unit {
measure: InvalidMeasure, measure: InvalidMeasure,
divMeasure: InvalidMeasure, divMeasure: InvalidMeasure,
} }
matches := prefixRegex.FindStringSubmatch(unitStr) matches := prefixUnitSplitRegex.FindStringSubmatch(unitStr)
if len(matches) > 2 { if len(matches) > 2 {
pre := NewPrefix(matches[1]) pre := NewPrefix(matches[1])
measures := strings.Split(matches[2], "/") measures := strings.Split(matches[2], "/")