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 == "" { return "" } return s default: return "" } }