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