- 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>
61 lines
1.5 KiB
Go
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
|
|
}
|