class_name SoundtrackSearch extends RefCounted ## Finds soundtrack names matching a search text. Matches ignore case, spaces and ## special characters, and also find the letters in order with others in between. ## ## Used by the Search popup and the statistics view, so both find the same soundtracks. ## Each name as a Dictionary with its original, lowercase and cleaned form. var _names: Array[Dictionary] = [] ## Compiled patterns for the letters-in-order search, by cleaned search term. var _regex_cache: Dictionary = {} ## Replaces the names to search in. func set_names(names: Array) -> void: _names.clear() for soundtrack: String in names: _names.append({ "original": soundtrack, "lowercase": soundtrack.to_lower(), "cleaned": clean_term(soundtrack), }) ## Returns the names matching [param search_text], best matches first: plain ## case-insensitive matches, then matches ignoring spaces and special characters, ## then matches with the letters in order but not next to each other. func find_matches(search_text: String) -> Array[String]: var seen: Dictionary = {} var search_text_lower: String = search_text.to_lower() var clean_search_text: String = clean_term(search_text) var regex: RegEx = _compile_regex_cached(clean_search_text) var passes: Array[Callable] = [ func(entry: Dictionary) -> bool: return entry.lowercase.contains(search_text_lower), func(entry: Dictionary) -> bool: return entry.cleaned.contains(clean_search_text), func(entry: Dictionary) -> bool: return regex.search(entry.cleaned) != null, ] var matches: Array[String] = [] for matches_pass: Callable in passes: for entry: Dictionary in _names: if !seen.has(entry.original) && matches_pass.call(entry): seen[entry.original] = true matches.append(entry.original) return matches ## Cleans a term by removing spaces and replacing special characters. ## Matches MusicServer's clean_term() function. static func clean_term(term: String) -> String: return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and") \ .replace("'n", "and").to_lower() # Compiles the letters-in-order pattern for [param search_term], once per term func _compile_regex_cached(search_term: String) -> RegEx: if not search_term in _regex_cache: var regex: RegEx = RegEx.new() var regex_text: String = ".*" for letter: String in search_term: # Escape regex metacharacters so searches like "Q*bert" or "(Remix)" work if "\\^$.|?*+()[]{}".contains(letter): regex_text += "\\" regex_text += letter + ".*" regex.compile(regex_text) _regex_cache[search_term] = regex return _regex_cache[search_term]