75 lines
2.1 KiB
Go
75 lines
2.1 KiB
Go
// Package metricparse extracts a numeric value from the free-text engine/metric strings
|
|
// used across FM reports (e.g. "GPC/N1 63.2"). The same parser MUST be used both when
|
|
// aggregating totals and when deriving a single report's "today" contribution, otherwise
|
|
// prev = total - today would not reconcile.
|
|
package metricparse
|
|
|
|
import (
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// numberToken matches a signed decimal number (integer or fractional).
|
|
var numberToken = regexp.MustCompile(`[-+]?\d*\.?\d+`)
|
|
|
|
// Parse returns ParseString(*v), or 0 when v is nil.
|
|
func Parse(v *string) float64 {
|
|
if v == nil {
|
|
return 0
|
|
}
|
|
return ParseString(*v)
|
|
}
|
|
|
|
// ParseString pulls the numeric VALUE out of a metric label string:
|
|
//
|
|
// "GPC/N1 63.2" -> 63.2 "PTC/N2 61.8" -> 61.8 "63.2" -> 63.2
|
|
// "63,2" -> 63.2 " -5 " -> -5 "N1" -> 0 (label only)
|
|
//
|
|
// It ignores digits fused to a label letter (the "1" in "N1", the "2" in "N2") by skipping
|
|
// any number immediately preceded by a letter, and prefers a token that carries a decimal
|
|
// point when several stand-alone numbers are present.
|
|
func ParseString(s string) float64 {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
// Normalize a decimal comma ("63,2") to a dot when it is the sole separator.
|
|
if !strings.Contains(s, ".") {
|
|
if i := strings.IndexByte(s, ','); i > 0 && i+1 < len(s) &&
|
|
isASCIIDigit(s[i-1]) && isASCIIDigit(s[i+1]) {
|
|
s = s[:i] + "." + s[i+1:]
|
|
}
|
|
}
|
|
|
|
locs := numberToken.FindAllStringIndex(s, -1)
|
|
candidates := make([]string, 0, len(locs))
|
|
for _, loc := range locs {
|
|
// A value is never glued to a preceding letter; that only happens for labels
|
|
// like N1/N2. Real values start the string or follow a space/slash/colon/etc.
|
|
if loc[0] > 0 && unicode.IsLetter(rune(s[loc[0]-1])) {
|
|
continue
|
|
}
|
|
candidates = append(candidates, s[loc[0]:loc[1]])
|
|
}
|
|
if len(candidates) == 0 {
|
|
return 0
|
|
}
|
|
|
|
chosen := candidates[0]
|
|
for _, c := range candidates {
|
|
if strings.Contains(c, ".") {
|
|
chosen = c
|
|
break
|
|
}
|
|
}
|
|
n, err := strconv.ParseFloat(chosen, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
func isASCIIDigit(b byte) bool { return b >= '0' && b <= '9' }
|