// 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; using System.Linq; namespace Microsoft.Plugin.WindowWalker.Components { /// /// Class housing fuzzy matching methods /// internal static class FuzzyMatching { /// /// Finds the best match (the one with the most /// number of letters adjacent to each other) and /// returns the index location of each of the letters /// of the matches /// /// The text to search inside of /// the text to search for /// returns the index location of each of the letters of the matches internal static List FindBestFuzzyMatch(string text, string searchText) { ArgumentNullException.ThrowIfNull(searchText); ArgumentNullException.ThrowIfNull(text); // Using CurrentCulture since this is user facing searchText = searchText.ToLower(CultureInfo.CurrentCulture); text = text.ToLower(CultureInfo.CurrentCulture); // Create a grid to march matches like // e.g. // a b c a d e c f g // a x x // c x x bool[,] matches = new bool[text.Length, searchText.Length]; for (int firstIndex = 0; firstIndex < text.Length; firstIndex++) { for (int secondIndex = 0; secondIndex < searchText.Length; secondIndex++) { matches[firstIndex, secondIndex] = searchText[secondIndex] == text[firstIndex] ? true : false; } } // use this table to get all the possible matches List> allMatches = GetAllMatchIndexes(matches); // return the score that is the max int maxScore = allMatches.Count > 0 ? CalculateScoreForMatches(allMatches[0]) : 0; List bestMatch = allMatches.Count > 0 ? allMatches[0] : new List(); foreach (var match in allMatches) { int score = CalculateScoreForMatches(match); if (score > maxScore) { bestMatch = match; maxScore = score; } } return bestMatch; } /// /// Gets all the possible matches to the search string with in the text /// /// a table showing the matches as generated by /// a two dimensional array with the first dimension the text and the second /// one the search string and each cell marked as an intersection between the two /// a list of the possible combinations that match the search text internal static List> GetAllMatchIndexes(bool[,] matches) { ArgumentNullException.ThrowIfNull(matches); List> results = new List>(); for (int secondIndex = 0; secondIndex < matches.GetLength(1); secondIndex++) { for (int firstIndex = 0; firstIndex < matches.GetLength(0); firstIndex++) { if (secondIndex == 0 && matches[firstIndex, secondIndex]) { results.Add(new List { firstIndex }); } else if (matches[firstIndex, secondIndex]) { var tempList = results.Where(x => x.Count == secondIndex && x[x.Count - 1] < firstIndex).Select(x => x.ToList()).ToList(); foreach (var pathSofar in tempList) { pathSofar.Add(firstIndex); } results.AddRange(tempList); } } results = results.Where(x => x.Count == secondIndex + 1).ToList(); } return results.Where(x => x.Count == matches.GetLength(1)).ToList(); } /// /// 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; } } }