Files
fm_be/internal/shared/pkg/metricparse/metricparse.go
2026-07-16 22:16:45 +07:00

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' }