init push
This commit is contained in:
74
internal/shared/pkg/metricparse/metricparse.go
Normal file
74
internal/shared/pkg/metricparse/metricparse.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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' }
|
||||
43
internal/shared/pkg/metricparse/metricparse_test.go
Normal file
43
internal/shared/pkg/metricparse/metricparse_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package metricparse
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseString(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want float64
|
||||
}{
|
||||
{"GPC/N1 63.2", 63.2}, // the "1" in N1 must NOT win
|
||||
{"PTC/N2 61.8", 61.8}, // the "2" in N2 must NOT win
|
||||
{"GPC/NG/N1 105.3", 105.3},
|
||||
{"63.2", 63.2},
|
||||
{" 63.2 ", 63.2},
|
||||
{"63", 63},
|
||||
{"63,2", 63.2}, // decimal comma
|
||||
{"GPC/N1: 63.2 %", 63.2}, // colon + trailing unit
|
||||
{"GPC/N1 63.2 / N2 61.5", 63.2}, // first standalone decimal
|
||||
{"-5", -5},
|
||||
{"+7.5", 7.5},
|
||||
{".5", 0.5},
|
||||
{"N1", 0}, // label only, no value
|
||||
{"N2", 0}, // label only
|
||||
{"", 0},
|
||||
{" ", 0},
|
||||
{"no numbers here", 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := ParseString(c.in); got != c.want {
|
||||
t.Errorf("ParseString(%q) = %v, want %v", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNilAndPtr(t *testing.T) {
|
||||
if got := Parse(nil); got != 0 {
|
||||
t.Fatalf("Parse(nil) = %v, want 0", got)
|
||||
}
|
||||
s := "GPC/N1 63.2"
|
||||
if got := Parse(&s); got != 63.2 {
|
||||
t.Fatalf("Parse(&%q) = %v, want 63.2", s, got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user