// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. // Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ using System; using System.Collections.Generic; using System.Globalization; namespace Microsoft.Plugin.WindowWalker.Components { /// /// Class housing fuzzy matching methods /// internal static class FuzzyMatching { /// /// Find the best match (the one with the smallest span) using a Dynamic Programming approach /// to minimize candidate matches. /// /// The text to search inside of. /// The text to search for. /// The index location of each of the letters in the best match. internal static List FindBestFuzzyMatch(string text, string searchText) { ArgumentNullException.ThrowIfNull(searchText); ArgumentNullException.ThrowIfNull(text); var sLower = searchText.ToLower(CultureInfo.CurrentCulture); var tLower = text.ToLower(CultureInfo.CurrentCulture); int m = sLower.Length; int n = tLower.Length; // A subsequence longer than the candidate text can never match. if (m > n) { return []; } // bestStart[k, i] stores the latest possible start index of a match for s[0..k] that // ends exactly at t[i], or -1 if no such match exists. // // Tracking the latest start ensures that we only retain the smallest span of all matches // that end at i. int[,] bestStart = new int[m, n]; // parent[k, i] stores the index where the previous character matched to allow for // reconstruction of the best path once the DP step completes. int[,] parent = new int[m, n]; // Initialize tables. for (int k = 0; k < m; k++) { for (int i = 0; i < n; i++) { bestStart[k, i] = -1; } } // Base case: match the first character of the search string s[0]. for (int i = 0; i < n; i++) { if (tLower[i] == sLower[0]) { bestStart[0, i] = i; parent[0, i] = -1; } } // Dynamic programming step: extend matches for the remaining characters s[1..m-1]. for (int k = 1; k < m; k++) { int currentMaxStart = -1; int currentParentIndex = -1; for (int i = 0; i < n; i++) { // 1. Try to match s[k] at t[i]. // We must use a valid start from the previous row (k-1) that appeared BEFORE i. // 'currentMaxStart' holds the best start value from indices 0 to i-1. if (tLower[i] == sLower[k]) { if (currentMaxStart != -1) { bestStart[k, i] = currentMaxStart; parent[k, i] = currentParentIndex; } } // 2. Maintain the dominating predecessor for the next column. // We only keep the match with the latest start index, as it strictly dominates // all earlier-starting matches for the purpose of minimizing the match span. if (bestStart[k - 1, i] > currentMaxStart) { currentMaxStart = bestStart[k - 1, i]; currentParentIndex = i; } } } // Select the ending position that minimizes span. int bestEndIndex = -1; int maxScore = int.MinValue; // Score logic: -(LastIndex - StartIndex). // We want to Maximize Score => Minimize Span. for (int i = 0; i < n; i++) { if (bestStart[m - 1, i] != -1) { int start = bestStart[m - 1, i]; int score = -(i - start); if (score > maxScore) { maxScore = score; bestEndIndex = i; } } } if (bestEndIndex == -1) { return []; } // Reconstruct only the winning path. var result = new List(m); int curr = bestEndIndex; for (int k = m - 1; k >= 0; k--) { result.Add(curr); curr = parent[k, curr]; } result.Reverse(); return result; } /// /// Calculates the score for a string /// /// the index of the matches /// an integer representing the score internal static int CalculateScoreForMatches(List matches) { ArgumentNullException.ThrowIfNull(matches); var score = 0; for (int currentIndex = 1; currentIndex < matches.Count; currentIndex++) { var previousIndex = currentIndex - 1; score -= matches[currentIndex] - matches[previousIndex]; } return score == 0 ? -10000 : score; } } }