Files
descrybe/apps/api/internal/processing/fill.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

160 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package processing
import (
"fmt"
"regexp"
"strings"
)
var (
brandPrefixRe = regexp.MustCompile(`(?i)^([A-Za-z][A-Za-z0-9&.\-]{1,40})\b`)
dimTripleRe = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)`)
dimPairRe = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)`)
weightRe = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)\s*(kg|g|lb|oz)\b`)
)
// FillMissingFields derives sensible standard fields from name/attrs when absent.
func FillMissingFields(mapped map[string]any, attrs map[string]any) map[string]any {
out := make(map[string]any, len(mapped)+8)
for k, v := range mapped {
out[k] = v
}
name := stringFromAny(out["name"])
if name == "" {
name = stringFromAny(out["title"])
}
if stringFromAny(out["brand"]) == "" {
if b := stringFromAny(attrs["brand"]); b != "" {
out["brand"] = b
} else if b := inferBrand(name); b != "" {
out["brand"] = b
}
}
if stringFromAny(out["gtin"]) == "" {
if g := stringFromAny(out["ean"]); g != "" {
out["gtin"] = g
}
}
blob := name + " " + stringFromAny(out["description"])
for _, m := range []map[string]any{attrs, out} {
for _, k := range []string{"dimensions", "size", "dimension"} {
blob += " " + stringFromAny(m[k])
}
}
if stringFromAny(out["width"]) == "" || stringFromAny(out["height"]) == "" || stringFromAny(out["depth"]) == "" {
if w, h, d, ok := parseDimensions(blob); ok {
if stringFromAny(out["width"]) == "" {
out["width"] = w
}
if stringFromAny(out["height"]) == "" {
out["height"] = h
}
if stringFromAny(out["depth"]) == "" && d != "" {
out["depth"] = d
}
}
}
if stringFromAny(out["weight"]) == "" {
if w := stringFromAny(attrs["weight"]); w != "" {
out["weight"] = w
} else if w, ok := parseWeight(blob); ok {
out["weight"] = w
}
}
if stringFromAny(out["category"]) == "" {
if c := stringFromAny(attrs["category"]); c != "" {
out["category"] = c
}
}
if stringFromAny(out["stock_status"]) == "" {
if s := stringFromAny(out["stock"]); s != "" {
out["stock_status"] = normalizeStockStatusLabel(s)
}
} else {
out["stock_status"] = normalizeStockStatusLabel(stringFromAny(out["stock_status"]))
}
return out
}
func inferBrand(name string) string {
name = strings.TrimSpace(name)
if name == "" {
return ""
}
m := brandPrefixRe.FindStringSubmatch(name)
if len(m) < 2 {
return ""
}
b := strings.TrimSpace(m[1])
// Skip generic leading words
switch strings.ToLower(b) {
case "the", "new", "set", "pack", "pair", "product", "item":
return ""
}
return SanitizeOutput(b)
}
func parseDimensions(blob string) (w, h, d string, ok bool) {
if m := dimTripleRe.FindStringSubmatch(blob); len(m) == 4 {
return normalizeNum(m[1]), normalizeNum(m[2]), normalizeNum(m[3]), true
}
if m := dimPairRe.FindStringSubmatch(blob); len(m) == 3 {
return normalizeNum(m[1]), normalizeNum(m[2]), "", true
}
return "", "", "", false
}
func parseWeight(blob string) (string, bool) {
m := weightRe.FindStringSubmatch(blob)
if len(m) < 3 {
return "", false
}
return normalizeNum(m[1]) + " " + strings.ToLower(m[2]), true
}
func normalizeNum(s string) string {
return strings.ReplaceAll(strings.TrimSpace(s), ",", ".")
}
func normalizeStockStatusLabel(s string) string {
if mapped := MapStockStatus(s); mapped != "" {
return mapped
}
s = strings.ToLower(strings.TrimSpace(s))
switch {
case s == "" || s == "0" || strings.Contains(s, "out"):
return "out_of_stock"
case strings.Contains(s, "pre"):
return "preorder"
case strings.Contains(s, "back"):
return "backorder"
default:
return "in_stock"
}
}
func stringFromAny(v any) string {
if v == nil {
return ""
}
switch t := v.(type) {
case string:
return strings.TrimSpace(t)
case float64, float32, int, int64, bool:
s := strings.TrimSpace(fmt.Sprint(t))
if s == "<nil>" {
return ""
}
return s
default:
return ""
}
}