// Package normalize provides string normalization helpers used for // fuzzy matching across NaviWatcher (artist names, album titles, etc.). // // It is the single shared home for normalization logic; previously this // lived inside the musicbrainz package but is needed by the scanner engine // and any other consumer that compares strings. package normalize import ( "regexp" "strings" "unicode" ) // Precompiled regexes — compiled once at package init. var ( bracketRe = regexp.MustCompile(`\[[^\]]*\]`) parenRe = regexp.MustCompile(`\([^)]*\)`) yearRe = regexp.MustCompile(`\b(1[0-9]{3}|2[0-9]{3})\b`) spaceRe = regexp.MustCompile(`\s+`) ) // NormalizeString normalizes a string for fuzzy matching by: // - Converting to lowercase // - Removing special characters (keeping only letters, digits, and spaces) // - Removing years (4-digit numbers that look like years) // - Removing bracketed keywords (e.g., [Deluxe], [Remastered]) // - Collapsing multiple spaces into one // - Trimming leading/trailing whitespace func NormalizeString(s string) string { // Convert to lowercase s = strings.ToLower(s) // Remove bracketed content first (e.g., [Deluxe Edition], [Remastered 2020]) s = bracketRe.ReplaceAllString(s, "") // Remove parenthesized content (e.g., (Deluxe), (Remastered)) s = parenRe.ReplaceAllString(s, "") // Remove years (4-digit numbers between 1000-2999). If stripping the // year would empty the entire string (e.g. an album literally titled // "1989" or "2112"), keep the original form so the title can still match. stripped := yearRe.ReplaceAllString(s, "") if strings.TrimSpace(stripped) != "" { s = stripped } // Replace common separators with spaces before stripping other special chars s = strings.ReplaceAll(s, "-", " ") s = strings.ReplaceAll(s, "_", " ") // Keep only letters, digits, and spaces var b strings.Builder for _, r := range s { if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) { b.WriteRune(r) } } s = b.String() // Collapse multiple spaces s = spaceRe.ReplaceAllString(s, " ") // Trim s = strings.TrimSpace(s) return s } // NormalizeArtistName normalizes an artist name for comparison. // It applies NormalizeString and additionally handles common prefixes. func NormalizeArtistName(name string) string { name = NormalizeString(name) // Remove common leading articles for better matching prefixes := []string{"the ", "a ", "an "} for _, prefix := range prefixes { if strings.HasPrefix(name, prefix) { name = strings.TrimPrefix(name, prefix) break } } return strings.TrimSpace(name) }