mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-27 05:49:04 +01:00
Integrate cc-units as units package
This commit is contained in:
parent
d8aaa26c27
commit
f6508046ff
174
pkg/units/README.md
Normal file
174
pkg/units/README.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# 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](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:
|
||||||
|
```go
|
||||||
|
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
|
||||||
|
String() string
|
||||||
|
Short() string
|
||||||
|
AddUnitDenominator(div Measure)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to get the "normalized" string unit back or test for validity, you can use:
|
||||||
|
```go
|
||||||
|
u := NewUnit("MB")
|
||||||
|
fmt.Println(u.Valid()) // true
|
||||||
|
fmt.Printf("Long string %q", u.String()) // MegaBytes
|
||||||
|
fmt.Printf("Short string %q", u.Short()) // MBytes
|
||||||
|
v := NewUnit("foo")
|
||||||
|
fmt.Println(v.Valid()) // false
|
||||||
|
```
|
||||||
|
|
||||||
|
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 := 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).
|
||||||
|
|
||||||
|
If you have a metric and want the derivation to a bandwidth or events per second, you can use the original unit:
|
||||||
|
|
||||||
|
```go
|
||||||
|
in_unit, err := metric.GetMeta("unit")
|
||||||
|
if err == nil {
|
||||||
|
value, ok := metric.GetField("value")
|
||||||
|
if ok {
|
||||||
|
out_unit = NewUnit(in_unit)
|
||||||
|
out_unit.AddUnitDenominator("seconds")
|
||||||
|
seconds := timeDiff.Seconds()
|
||||||
|
y, err := lp.New(metric.Name()+"_bw",
|
||||||
|
metric.Tags(),
|
||||||
|
metric.Meta(),
|
||||||
|
map[string]interface{"value": value/seconds},
|
||||||
|
metric.Time())
|
||||||
|
if err == nil {
|
||||||
|
y.AddMeta("unit", out_unit.Short())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special unit detection
|
||||||
|
|
||||||
|
Some used measures like Bytes and Flops are non-dividable. Consequently there prefixes like Milli, Micro and Nano are not useful. This is quite handy since a unit `mb` for `MBytes` is not uncommon but would by default be parsed as "MilliBytes".
|
||||||
|
|
||||||
|
Special parsing rules for the following measures: iff `prefix==Milli`, use `prefix==Mega`
|
||||||
|
- `Bytes`
|
||||||
|
- `Flops`
|
||||||
|
- `Packets`
|
||||||
|
- `Events`
|
||||||
|
- `Cycles`
|
||||||
|
- `Requests`
|
||||||
|
|
||||||
|
This means the prefixes `Micro` (like `ubytes`) and `Nano` like (`nflops/sec`) are not allowed and return an invalid unit. But you can specify `mflops` and `mb`.
|
||||||
|
|
||||||
|
Prefixes for `%` or `percent` are ignored.
|
||||||
|
|
||||||
|
## Supported prefixes
|
||||||
|
|
||||||
|
```go
|
||||||
|
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
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The prefixes are detected using a regular expression `^([kKmMgGtTpP]?[i]?)(.*)` that splits the prefix from the measure. You probably don't need to deal with the prefixes in the code.
|
||||||
|
|
||||||
|
## Supported measures
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
None Measure = iota
|
||||||
|
Bytes
|
||||||
|
Flops
|
||||||
|
Percentage
|
||||||
|
TemperatureC
|
||||||
|
TemperatureF
|
||||||
|
Rotation
|
||||||
|
Hertz
|
||||||
|
Time
|
||||||
|
Watt
|
||||||
|
Joule
|
||||||
|
Cycles
|
||||||
|
Requests
|
||||||
|
Packets
|
||||||
|
Events
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
There a regular expression for each of the measures like `^([bB][yY]?[tT]?[eE]?[sS]?)` for the `Bytes` measure.
|
||||||
|
|
||||||
|
|
||||||
|
## New units
|
||||||
|
|
||||||
|
If the selected units are not suitable for your metric, feel free to send a PR.
|
||||||
|
|
||||||
|
### New prefix
|
||||||
|
|
||||||
|
For a new prefix, add it to the big `const` in `ccUnitPrefix.go` and adjust the prefix-unit-splitting regular expression. Afterwards, you have to add cases to the three functions `String()`, `Prefix()` and `NewPrefix()`. `NewPrefix()` contains the parser (`k` or `K` -> `Kilo`). The other one are used for output. `String()` outputs a longer version of the prefix (`Kilo`), while `Prefix()` returns only the short notation (`K`).
|
||||||
|
|
||||||
|
### New measure
|
||||||
|
|
||||||
|
Adding new prefixes is probably rare but adding a new measure is a more common task. At first, add it to the big `const` in `ccUnitMeasure.go`. Moreover, create a regular expression matching the measure (and pre-compile it like the others). Add the expression matching to `NewMeasure()`. The `String()` and `Short()` functions return descriptive strings for the measure in long form (like `Hertz`) and short form (like `Hz`).
|
||||||
|
|
||||||
|
If there are special conversation rules between measures and you want to convert one measure to another, like temperatures in Celsius to Fahrenheit, a special case in `GetUnitPrefixFactor()` is required.
|
||||||
|
|
||||||
|
### Special parsing rules
|
||||||
|
|
||||||
|
The two parsers for prefix and measure are called under the hood by `NewUnit()` and there might some special rules apply. Like in the above section about 'special unit detection', special rules for your new measure might be required. Currently there are two special cases:
|
||||||
|
|
||||||
|
- Measures that are non-dividable like Flops, Bytes, Events, ... cannot use `Milli`, `Micro` and `Nano`. The prefix `m` is forced to `M` for these measures
|
||||||
|
- If the prefix is `p`/`P` (`Peta`) or `e`/`E` (`Exa`) and the measure is not detectable, it retries detection with the prefix. So first round it tries, for example, prefix `p` and measure `ackets` which fails, so it retries the detection with measure `packets` and `<empty>` prefix (resolves to `Base` prefix).
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
The `ccUnits` package is a simple implemtation of a unit system and comes with some limitations:
|
||||||
|
|
||||||
|
- The unit denominator (like `s` in `Mbyte/s`) can only have the `Base` prefix, you cannot specify `Byte/ms` for "Bytes per milli second".
|
134
pkg/units/unitMeasure.go
Normal file
134
pkg/units/unitMeasure.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package units
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
type Measure int
|
||||||
|
|
||||||
|
const (
|
||||||
|
InvalidMeasure Measure = iota
|
||||||
|
Bytes
|
||||||
|
Flops
|
||||||
|
Percentage
|
||||||
|
TemperatureC
|
||||||
|
TemperatureF
|
||||||
|
Rotation
|
||||||
|
Frequency
|
||||||
|
Time
|
||||||
|
Watt
|
||||||
|
Joule
|
||||||
|
Cycles
|
||||||
|
Requests
|
||||||
|
Packets
|
||||||
|
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'
|
||||||
|
func (m *Measure) String() string {
|
||||||
|
if data, ok := MeasuresMap[*m]; ok {
|
||||||
|
return data.Long
|
||||||
|
}
|
||||||
|
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().
|
||||||
|
func (m *Measure) Short() string {
|
||||||
|
if data, ok := MeasuresMap[*m]; ok {
|
||||||
|
return data.Short
|
||||||
|
}
|
||||||
|
return InvalidMeasureShort
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMeasure creates a new measure out of a string representing a measure like 'Bytes', 'Flops' and 'precent'.
|
||||||
|
// It uses regular expressions for matching.
|
||||||
|
func NewMeasure(unit string) Measure {
|
||||||
|
for m, data := range MeasuresMap {
|
||||||
|
regex := regexp.MustCompile(data.Regex)
|
||||||
|
match := regex.FindStringSubmatch(unit)
|
||||||
|
if match != nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return InvalidMeasure
|
||||||
|
}
|
174
pkg/units/unitPrefix.go
Normal file
174
pkg/units/unitPrefix.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package units
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Prefix float64
|
||||||
|
|
||||||
|
const (
|
||||||
|
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 PrefixUnitSplitRegexStr = `^([kKmMgGtTpPeEzZyY]?[i]?)(.*)`
|
||||||
|
|
||||||
|
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'
|
||||||
|
func (p *Prefix) String() string {
|
||||||
|
if data, ok := PrefixDataMap[*p]; ok {
|
||||||
|
return data.Long
|
||||||
|
}
|
||||||
|
return InvalidMeasureLong
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix returns the short string for the prefix like 'K', 'M' or 'G'. Is is recommened to use Prefix() over String().
|
||||||
|
func (p *Prefix) Prefix() string {
|
||||||
|
if data, ok := PrefixDataMap[*p]; ok {
|
||||||
|
return data.Short
|
||||||
|
}
|
||||||
|
return InvalidMeasureShort
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrefix creates a new prefix out of a string representing a unit like 'k', 'K', 'M' or 'G'.
|
||||||
|
func NewPrefix(prefix string) Prefix {
|
||||||
|
for p, data := range PrefixDataMap {
|
||||||
|
regex := regexp.MustCompile(data.Regex)
|
||||||
|
match := regex.FindStringSubmatch(prefix)
|
||||||
|
if match != nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return InvalidPrefix
|
||||||
|
}
|
250
pkg/units/units.go
Normal file
250
pkg/units/units.go
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
// Unit system for cluster monitoring metrics like bytes, flops and events
|
||||||
|
package units
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type unit struct {
|
||||||
|
prefix Prefix
|
||||||
|
measure Measure
|
||||||
|
divMeasure Measure
|
||||||
|
}
|
||||||
|
|
||||||
|
type Unit interface {
|
||||||
|
Valid() bool
|
||||||
|
String() string
|
||||||
|
Short() string
|
||||||
|
AddUnitDenominator(div Measure)
|
||||||
|
getPrefix() Prefix
|
||||||
|
getMeasure() Measure
|
||||||
|
getUnitDenominator() Measure
|
||||||
|
setPrefix(p Prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
var INVALID_UNIT = NewUnit("foobar")
|
||||||
|
|
||||||
|
// Valid checks whether a unit is a valid unit. A unit is valid if it has at least a prefix and a measure. The unit denominator is optional.
|
||||||
|
func (u *unit) Valid() bool {
|
||||||
|
return u.prefix != InvalidPrefix && u.measure != InvalidMeasure
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the long string for the unit like 'KiloHertz' or 'MegaBytes'
|
||||||
|
func (u *unit) String() string {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short returns the short string for the unit like 'kHz' or 'MByte'. Is is recommened to use Short() over String().
|
||||||
|
func (u *unit) Short() string {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUnitDenominator adds a unit denominator to an exising unit. Can be used if you want to derive e.g. data volume to bandwidths.
|
||||||
|
// The data volume is in a Byte unit like 'kByte' and by dividing it by the runtime in seconds, we get the bandwidth. We can use the
|
||||||
|
// data volume unit and add 'Second' as unit denominator
|
||||||
|
func (u *unit) AddUnitDenominator(div Measure) {
|
||||||
|
u.divMeasure = div
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *unit) getUnitDenominator() Measure {
|
||||||
|
return u.divMeasure
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefixPrefixFactor creates the default conversion function between two prefixes.
|
||||||
|
// It returns a conversation function for the value.
|
||||||
|
func GetPrefixPrefixFactor(in Prefix, out Prefix) func(value interface{}) interface{} {
|
||||||
|
var factor = 1.0
|
||||||
|
var in_prefix = float64(in)
|
||||||
|
var out_prefix = float64(out)
|
||||||
|
factor = in_prefix / out_prefix
|
||||||
|
conv := func(value interface{}) interface{} {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return v * factor
|
||||||
|
case float32:
|
||||||
|
return float32(float64(v) * factor)
|
||||||
|
case int:
|
||||||
|
return int(float64(v) * factor)
|
||||||
|
case int32:
|
||||||
|
return int32(float64(v) * factor)
|
||||||
|
case int64:
|
||||||
|
return int64(float64(v) * factor)
|
||||||
|
case uint:
|
||||||
|
return uint(float64(v) * factor)
|
||||||
|
case uint32:
|
||||||
|
return uint32(float64(v) * factor)
|
||||||
|
case uint64:
|
||||||
|
return uint64(float64(v) * factor)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return conv
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the conversion function between temperatures in Celsius to Fahrenheit
|
||||||
|
func convertTempC2TempF(value interface{}) interface{} {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return (v * 1.8) + 32
|
||||||
|
case float32:
|
||||||
|
return (v * 1.8) + 32
|
||||||
|
case int:
|
||||||
|
return int((float64(v) * 1.8) + 32)
|
||||||
|
case int32:
|
||||||
|
return int32((float64(v) * 1.8) + 32)
|
||||||
|
case int64:
|
||||||
|
return int64((float64(v) * 1.8) + 32)
|
||||||
|
case uint:
|
||||||
|
return uint((float64(v) * 1.8) + 32)
|
||||||
|
case uint32:
|
||||||
|
return uint32((float64(v) * 1.8) + 32)
|
||||||
|
case uint64:
|
||||||
|
return uint64((float64(v) * 1.8) + 32)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the conversion function between temperatures in Fahrenheit to Celsius
|
||||||
|
func convertTempF2TempC(value interface{}) interface{} {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return (v - 32) / 1.8
|
||||||
|
case float32:
|
||||||
|
return (v - 32) / 1.8
|
||||||
|
case int:
|
||||||
|
return int(((float64(v) - 32) / 1.8))
|
||||||
|
case int32:
|
||||||
|
return int32(((float64(v) - 32) / 1.8))
|
||||||
|
case int64:
|
||||||
|
return int64(((float64(v) - 32) / 1.8))
|
||||||
|
case uint:
|
||||||
|
return uint(((float64(v) - 32) / 1.8))
|
||||||
|
case uint32:
|
||||||
|
return uint32(((float64(v) - 32) / 1.8))
|
||||||
|
case uint64:
|
||||||
|
return uint64(((float64(v) - 32) / 1.8))
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefixStringPrefixStringFactor is a wrapper for GetPrefixPrefixFactor with string inputs instead
|
||||||
|
// of prefixes. It also returns a conversation function for the value.
|
||||||
|
func GetPrefixStringPrefixStringFactor(in string, out string) func(value interface{}) interface{} {
|
||||||
|
var i Prefix = NewPrefix(in)
|
||||||
|
var o Prefix = NewPrefix(out)
|
||||||
|
return GetPrefixPrefixFactor(i, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUnitPrefixFactor gets the conversion function and resulting unit for a unit and a prefix. This is
|
||||||
|
// the most common case where you have some input unit and want to convert it to the same unit but with
|
||||||
|
// a different prefix. The returned unit represents the value after conversation.
|
||||||
|
func GetUnitPrefixFactor(in Unit, out Prefix) (func(value interface{}) interface{}, Unit) {
|
||||||
|
outUnit := NewUnit(in.Short())
|
||||||
|
if outUnit.Valid() {
|
||||||
|
outUnit.setPrefix(out)
|
||||||
|
conv := GetPrefixPrefixFactor(in.getPrefix(), out)
|
||||||
|
return conv, outUnit
|
||||||
|
}
|
||||||
|
return nil, INVALID_UNIT
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUnitPrefixStringFactor gets the conversion function and resulting unit for a unit and a prefix as string.
|
||||||
|
// It is a wrapper for GetUnitPrefixFactor
|
||||||
|
func GetUnitPrefixStringFactor(in Unit, out string) (func(value interface{}) interface{}, Unit) {
|
||||||
|
var o Prefix = NewPrefix(out)
|
||||||
|
return GetUnitPrefixFactor(in, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUnitStringPrefixStringFactor gets the conversion function and resulting unit for a unit and a prefix when both are only string representations.
|
||||||
|
// This is just a wrapper for GetUnitPrefixFactor with the given input unit and the desired output prefix.
|
||||||
|
func GetUnitStringPrefixStringFactor(in string, out string) (func(value interface{}) interface{}, Unit) {
|
||||||
|
var i = NewUnit(in)
|
||||||
|
return GetUnitPrefixStringFactor(i, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUnitUnitFactor gets the conversion function and (maybe) error for unit to unit conversion.
|
||||||
|
// It is basically a wrapper for GetPrefixPrefixFactor with some special cases for temperature
|
||||||
|
// conversion between Fahrenheit and Celsius.
|
||||||
|
func GetUnitUnitFactor(in Unit, out Unit) (func(value interface{}) interface{}, error) {
|
||||||
|
if in.getMeasure() == TemperatureC && out.getMeasure() == TemperatureF {
|
||||||
|
return convertTempC2TempF, nil
|
||||||
|
} else if in.getMeasure() == TemperatureF && out.getMeasure() == TemperatureC {
|
||||||
|
return convertTempF2TempC, nil
|
||||||
|
} else if in.getMeasure() != out.getMeasure() || in.getUnitDenominator() != out.getUnitDenominator() {
|
||||||
|
return func(value interface{}) interface{} { return 1.0 }, fmt.Errorf("invalid measures in in and out Unit")
|
||||||
|
}
|
||||||
|
return GetPrefixPrefixFactor(in.getPrefix(), out.getPrefix()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnit creates a new unit out of a string representing a unit like 'Mbyte/s' or 'GHz'.
|
||||||
|
// It uses regular expressions to detect the prefix, unit and (maybe) unit denominator.
|
||||||
|
func NewUnit(unitStr string) Unit {
|
||||||
|
u := &unit{
|
||||||
|
prefix: InvalidPrefix,
|
||||||
|
measure: InvalidMeasure,
|
||||||
|
divMeasure: InvalidMeasure,
|
||||||
|
}
|
||||||
|
matches := prefixUnitSplitRegex.FindStringSubmatch(unitStr)
|
||||||
|
if len(matches) > 2 {
|
||||||
|
pre := NewPrefix(matches[1])
|
||||||
|
measures := strings.Split(matches[2], "/")
|
||||||
|
m := NewMeasure(measures[0])
|
||||||
|
// 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 == InvalidMeasure {
|
||||||
|
switch pre {
|
||||||
|
case Peta, Exa:
|
||||||
|
t := NewMeasure(matches[1] + measures[0])
|
||||||
|
if t != InvalidMeasure {
|
||||||
|
m = t
|
||||||
|
pre = Base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div := InvalidMeasure
|
||||||
|
if len(measures) > 1 {
|
||||||
|
div = NewMeasure(measures[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m {
|
||||||
|
// Special case for 'm' as prefix for Bytes and some others as thers is no unit like MilliBytes
|
||||||
|
case Bytes, Flops, Packets, Events, Cycles, Requests:
|
||||||
|
if pre == Milli {
|
||||||
|
pre = Mega
|
||||||
|
}
|
||||||
|
// Special case for percentage. No/ignore prefix
|
||||||
|
case Percentage:
|
||||||
|
pre = Base
|
||||||
|
}
|
||||||
|
if pre != InvalidPrefix && m != InvalidMeasure {
|
||||||
|
u.prefix = pre
|
||||||
|
u.measure = m
|
||||||
|
if div != InvalidMeasure {
|
||||||
|
u.divMeasure = div
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
201
pkg/units/units_test.go
Normal file
201
pkg/units/units_test.go
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
package units
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"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")},
|
||||||
|
{"GHz", 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")},
|
||||||
|
{"events", NewUnit("events")},
|
||||||
|
{"event", NewUnit("events")},
|
||||||
|
{"EveNts", NewUnit("events")},
|
||||||
|
{"reqs", NewUnit("requests")},
|
||||||
|
{"reQuEsTs", NewUnit("requests")},
|
||||||
|
{"Requests", NewUnit("requests")},
|
||||||
|
{"cyc", NewUnit("cycles")},
|
||||||
|
{"cy", NewUnit("cycles")},
|
||||||
|
{"Cycles", NewUnit("cycles")},
|
||||||
|
{"J", NewUnit("Joules")},
|
||||||
|
{"Joule", NewUnit("Joules")},
|
||||||
|
{"joule", NewUnit("Joules")},
|
||||||
|
{"W", NewUnit("Watt")},
|
||||||
|
{"Watts", NewUnit("Watt")},
|
||||||
|
{"watt", NewUnit("Watt")},
|
||||||
|
{"s", NewUnit("seconds")},
|
||||||
|
{"sec", NewUnit("seconds")},
|
||||||
|
{"secs", NewUnit("seconds")},
|
||||||
|
{"RPM", NewUnit("rpm")},
|
||||||
|
{"rPm", NewUnit("rpm")},
|
||||||
|
{"watt/byte", NewUnit("W/B")},
|
||||||
|
{"watts/bytes", NewUnit("W/B")},
|
||||||
|
{"flop/byte", NewUnit("flops/Bytes")},
|
||||||
|
{"F/B", NewUnit("flops/Bytes")},
|
||||||
|
}
|
||||||
|
compareUnitExact := func(in, out Unit) bool {
|
||||||
|
if in.getMeasure() == out.getMeasure() && in.getUnitDenominator() == out.getUnitDenominator() && in.getPrefix() == out.getPrefix() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, c := range testCases {
|
||||||
|
u := NewUnit(c.in)
|
||||||
|
if (!u.Valid()) || (!compareUnitExact(u, c.want)) {
|
||||||
|
t.Errorf("func NewUnit(%q) == %q, want %q", c.in, u.String(), c.want.String())
|
||||||
|
} else {
|
||||||
|
t.Logf("NewUnit(%q) == %q", c.in, u.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnitUnitConversion(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
in string
|
||||||
|
want Unit
|
||||||
|
prefixFactor 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},
|
||||||
|
}
|
||||||
|
compareUnitWithPrefix := func(in, out Unit, factor float64) bool {
|
||||||
|
if in.getMeasure() == out.getMeasure() && in.getUnitDenominator() == out.getUnitDenominator() {
|
||||||
|
if f := GetPrefixPrefixFactor(in.getPrefix(), out.getPrefix()); f(1.0) == factor {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
fmt.Println(f(1.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, c := range testCases {
|
||||||
|
u := NewUnit(c.in)
|
||||||
|
if (!u.Valid()) || (!compareUnitWithPrefix(u, c.want, c.prefixFactor)) {
|
||||||
|
t.Errorf("GetPrefixPrefixFactor(%q, %q) invalid, want %q with factor %g", c.in, u.String(), c.want.String(), c.prefixFactor)
|
||||||
|
} else {
|
||||||
|
t.Logf("GetPrefixPrefixFactor(%q, %q) = %g", c.in, 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 %g", c.in, p.Prefix(), c.wantUnit.String(), c.prefixFactor)
|
||||||
|
} else {
|
||||||
|
t.Logf("GetUnitPrefixFactor(%q, %q) = %g", c.in, 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
|
||||||
|
}
|
||||||
|
for _, c := range testCases {
|
||||||
|
i := NewPrefix(c.in)
|
||||||
|
o := NewPrefix(c.want)
|
||||||
|
if i != InvalidPrefix && o != InvalidPrefix {
|
||||||
|
conv := GetPrefixPrefixFactor(i, o)
|
||||||
|
value := conv(1.0)
|
||||||
|
if value != c.prefixFactor {
|
||||||
|
t.Errorf("GetPrefixPrefixFactor(%q, %q) invalid, want %q with factor %g but got %g", c.in, c.want, o.Prefix(), c.prefixFactor, value)
|
||||||
|
} else {
|
||||||
|
t.Logf("GetPrefixPrefixFactor(%q, %q) = %g", c.in, c.want, c.prefixFactor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMeasureRegex(t *testing.T) {
|
||||||
|
for _, data := range MeasuresMap {
|
||||||
|
_, err := regexp.Compile(data.Regex)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to compile regex '%s': %s", data.Regex, err.Error())
|
||||||
|
}
|
||||||
|
t.Logf("succussfully compiled regex '%s' for measure %s", data.Regex, data.Long)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrefixRegex(t *testing.T) {
|
||||||
|
for _, data := range PrefixDataMap {
|
||||||
|
_, err := regexp.Compile(data.Regex)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to compile regex '%s': %s", data.Regex, err.Error())
|
||||||
|
}
|
||||||
|
t.Logf("succussfully compiled regex '%s' for prefix %s", data.Regex, data.Long)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user