149 lines
3.9 KiB
Go
149 lines
3.9 KiB
Go
package processing
|
||
|
||
import (
|
||
"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 c := categoryUniqueIDFromAny(out["category"]); c != "" {
|
||
out["category"] = c
|
||
} else if c := categoryUniqueIDFromAny(attrs["category"]); c != "" {
|
||
out["category"] = c
|
||
} else if c := categoryUniqueIDFromAny(out["category_unique_id"]); 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 {
|
||
// Flatten arrays / #text wrappers; callers that need HTML-stripped descriptions
|
||
// should use PlainDescriptionFromAny (normalize + V1 enforce do that).
|
||
return coerceScalarText(v)
|
||
} |