Files
PriceForge/internal/services/glob.go
Mikhail Chusavitin f73e3d144d Vendor mapping: wildcard ignore patterns, bulk CSV import, multi-lot qty
- Add glob pattern support (* and ?) for ignore rules stored in
  qt_vendor_partnumber_seen (is_pattern flag, migration 041)
- Pattern matching applied in stock/competitor import, partnumber book
  snapshot, and vendor mappings list (Go-side via NormalizeKey)
- BulkUpsertMappings: replace N+1 loop with two batch SQL upserts,
  validating all lots in a single query (~1500 queries → 3-4)
- CSV import: multi-lot per PN via repeated rows, optional qty column
- CSV export: updated column format vendor;partnumber;lot_name;qty;description;ignore;notes
- UI: ignore patterns section with add/delete, import progress feedback
- Update bible-local/vendor-mapping.md with new CSV format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 09:41:48 +03:00

61 lines
1.5 KiB
Go

package services
import "strings"
// isGlobPattern reports whether s contains wildcard characters (* or ?).
func isGlobPattern(s string) bool {
return strings.ContainsAny(s, "*?")
}
// matchGlob reports whether text matches the glob pattern.
// '*' matches any sequence of characters (including empty).
// '?' matches exactly one character.
// Both pattern and text must already be normalized (e.g. lowercased, separators removed)
// by the caller before passing to this function.
func matchGlob(pattern, text string) bool {
p, t := 0, 0
lastStar, lastMatch := -1, 0
for t < len(text) {
if p < len(pattern) && (pattern[p] == '?' || pattern[p] == text[t]) {
p++
t++
} else if p < len(pattern) && pattern[p] == '*' {
lastStar = p
lastMatch = t
p++
} else if lastStar >= 0 {
p = lastStar + 1
lastMatch++
t = lastMatch
} else {
return false
}
}
for p < len(pattern) && pattern[p] == '*' {
p++
}
return p == len(pattern)
}
// ignoreIndex holds both exact and pattern-based ignore rules.
type ignoreIndex struct {
exact map[string]struct{} // normalized exact partnumbers
patterns []string // normalized glob patterns
}
// isIgnored checks whether a normalized partnumber key is ignored (exact or pattern).
func (idx *ignoreIndex) isIgnored(normalizedPN string) bool {
if normalizedPN == "" {
return false
}
if _, ok := idx.exact[normalizedPN]; ok {
return true
}
for _, pat := range idx.patterns {
if matchGlob(pat, normalizedPN) {
return true
}
}
return false
}