Files
2026-08-16 16:57:36 +02:00

270 lines
6.5 KiB
Go

package processing
import (
"fmt"
"strings"
"unicode"
)
// knownAttrKeyAliases maps compact feed/spec tokens onto compact category
// attribute_key tokens (Slovenian catalog keys + common English synonyms).
var knownAttrKeyAliases = map[string]string{
"velikostzaslona": "diagonalazaslona",
"screensize": "diagonalazaslona",
"screen": "diagonalazaslona",
"diagonal": "diagonalazaslona",
"diagonala": "diagonalazaslona",
"nastavljivpovisini": "nastavljivopovisini",
"heightadjustable": "nastavljivopovisini",
"zaslonnadotik": "zaslonobcutljivnadotik",
"touchscreen": "zaslonobcutljivnadotik",
"tipmatrike": "vrstapanela",
"tippanela": "vrstapanela",
"paneltype": "vrstapanela",
"panel": "vrstapanela",
"zvocniki": "vgrajenizvocniki",
"speakers": "vgrajenizvocniki",
"ukrivljenzaslon": "oblikazaslona",
"curved": "oblikazaslona",
"slusalkezmikrofonom": "vgrajenmikrofon",
"mikrofon": "vgrajenmikrofon",
"builtinmic": "vgrajenmikrofon",
"microphone": "vgrajenmikrofon",
"brezzicne": "brezzicnapovezava",
"brezzicna": "brezzicnapovezava",
"wireless": "brezzicnapovezava",
"bluetooth": "brezzicnatehnologija",
"bt": "brezzicnatehnologija",
"anc": "odpravljanjehrupa",
"noisecancelling": "odpravljanjehrupa",
"noisecanceling": "odpravljanjehrupa",
"odpravljanjehrupa": "odpravljanjehrupa",
"resolution": "locljivost",
"responsetime": "odzivnicas",
}
// MapAttrsOntoAllowedKeys remaps feed/spec keys onto category_attributes keys
// when a confident match exists (exact compact, known alias, or unique token
// subset). Existing allowed/core values are never overwritten. When allowed is
// nil or empty, attrs are returned unchanged (caller still filters).
func MapAttrsOntoAllowedKeys(attrs map[string]any, allowed map[string]struct{}) map[string]any {
if len(attrs) == 0 || len(allowed) == 0 {
return attrs
}
prefer := preferredAllowedKeys(allowed)
if len(prefer) == 0 {
return attrs
}
out := make(map[string]any, len(attrs)+len(prefer))
for k, v := range attrs {
out[k] = v
}
for k, v := range attrs {
if !attrValuePresent(v) {
continue
}
canon := canonicalizeAttrKey(k)
if canon != "" {
if _, ok := coreCharacteristicAttrKeys[canon]; ok {
continue
}
if _, ok := allowed[canon]; ok {
continue
}
if _, ok := allowed[strings.ToLower(strings.TrimSpace(k))]; ok {
continue
}
}
target := resolveAllowedAttrKey(k, prefer)
if target == "" || target == canon || target == k {
continue
}
if attrValuePresent(out[target]) {
continue
}
out[target] = v
}
return out
}
func preferredAllowedKeys(allowed map[string]struct{}) map[string]string {
// compact → preferred output key (canonical snake_case when possible)
prefer := make(map[string]string, len(allowed))
for k := range allowed {
c := compactAttrToken(k)
if c == "" {
continue
}
canon := canonicalizeAttrKey(k)
if canon == "" {
canon = strings.ToLower(strings.TrimSpace(k))
}
if prev, ok := prefer[c]; ok {
// Prefer underscore form over hyphen duplicates from loadCategoryAttributeKeySets.
if strings.Contains(canon, "_") && !strings.Contains(prev, "_") {
prefer[c] = canon
}
continue
}
prefer[c] = canon
}
return prefer
}
func resolveAllowedAttrKey(feedKey string, prefer map[string]string) string {
ck := compactAttrToken(feedKey)
if ck == "" {
return ""
}
if target, ok := prefer[ck]; ok {
return target
}
if alias, ok := knownAttrKeyAliases[ck]; ok {
if target, ok := prefer[alias]; ok {
return target
}
}
return softMatchAllowedKey(feedKey, ck, prefer)
}
func softMatchAllowedKey(feedKey, feedCompact string, prefer map[string]string) string {
feedTokens := attrKeyTokens(feedKey)
if len(feedTokens) == 0 && feedCompact == "" {
return ""
}
var hits []string
for compact, target := range prefer {
if feedCompact != "" && (strings.Contains(compact, feedCompact) || strings.Contains(feedCompact, compact)) {
// Require meaningful overlap (avoid tiny tokens matching everything).
shorter, longer := feedCompact, compact
if len(shorter) > len(longer) {
shorter, longer = longer, shorter
}
if len(shorter) >= 6 && len(shorter)*10 >= len(longer)*6 {
hits = append(hits, target)
continue
}
}
if len(feedTokens) == 0 {
continue
}
allowedTokens := attrKeyTokens(target)
if len(allowedTokens) == 0 {
allowedTokens = attrKeyTokens(compact)
}
if tokensSubsetMatch(feedTokens, allowedTokens) {
hits = append(hits, target)
}
}
if len(hits) != 1 {
return ""
}
return hits[0]
}
func tokensSubsetMatch(feed, allowed []string) bool {
significant := 0
for _, ft := range feed {
if len(ft) < 3 {
continue
}
significant++
ok := false
for _, at := range allowed {
if at == ft || strings.HasPrefix(at, ft) || strings.HasPrefix(ft, at) {
ok = true
break
}
}
if !ok {
return false
}
}
return significant >= 2
}
func attrKeyTokens(k string) []string {
k = strings.ToLower(strings.TrimSpace(k))
if k == "" {
return nil
}
var b strings.Builder
var out []string
flush := func() {
if b.Len() == 0 {
return
}
tok := b.String()
b.Reset()
tok = compactAttrToken(tok)
if len(tok) >= 2 {
out = append(out, tok)
}
}
for _, r := range k {
r = foldAttrRune(r)
switch {
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
b.WriteRune(r)
default:
flush()
}
}
flush()
return out
}
func compactAttrToken(k string) string {
var b strings.Builder
b.Grow(len(k))
for _, r := range strings.ToLower(k) {
r = foldAttrRune(r)
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
}
}
return b.String()
}
func foldAttrRune(r rune) rune {
switch r {
case 'š', 'ś', 'ş':
return 's'
case 'č', 'ć', 'ç':
return 'c'
case 'ž', 'ź', 'ż':
return 'z'
case 'đ':
return 'd'
case 'ä', 'á', 'à', 'â', 'ã', 'å':
return 'a'
case 'ë', 'é', 'è', 'ê':
return 'e'
case 'ï', 'í', 'ì', 'î':
return 'i'
case 'ö', 'ó', 'ò', 'ô', 'õ':
return 'o'
case 'ü', 'ú', 'ù', 'û':
return 'u'
case 'ý', 'ÿ':
return 'y'
}
if unicode.IsLetter(r) {
return unicode.ToLower(r)
}
return r
}
func attrValuePresent(v any) bool {
if v == nil {
return false
}
s := strings.TrimSpace(fmt.Sprint(v))
return s != "" && s != "<nil>"
}