Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace Microsoft.Plugin.WindowWalker.Components
{
Expand All @@ -16,99 +15,128 @@ namespace Microsoft.Plugin.WindowWalker.Components
internal static class FuzzyMatching
{
/// <summary>
/// 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
/// Find the best match (the one with the smallest span) using a Dynamic Programming approach
/// to minimize candidate matches.
/// </summary>
/// <param name="text">The text to search inside of</param>
/// <param name="searchText">the text to search for</param>
/// <returns>returns the index location of each of the letters of the matches</returns>
/// <param name="text">The text to search inside of.</param>
/// <param name="searchText">The text to search for.</param>
/// <returns>The index location of each of the letters in the best match.</returns>
internal static List<int> 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++)
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)
{
for (int secondIndex = 0; secondIndex < searchText.Length; secondIndex++)
{
matches[firstIndex, secondIndex] =
searchText[secondIndex] == text[firstIndex] ?
true :
false;
}
return [];
}

// use this table to get all the possible matches
List<List<int>> allMatches = GetAllMatchIndexes(matches);
// 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];

// return the score that is the max
int maxScore = allMatches.Count > 0 ? CalculateScoreForMatches(allMatches[0]) : 0;
List<int> bestMatch = allMatches.Count > 0 ? allMatches[0] : new List<int>();
// 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];

foreach (var match in allMatches)
// Initialize tables.
for (int k = 0; k < m; k++)
{
int score = CalculateScoreForMatches(match);
if (score > maxScore)
for (int i = 0; i < n; i++)
{
bestMatch = match;
maxScore = score;
bestStart[k, i] = -1;
}
}

return bestMatch;
}

/// <summary>
/// Gets all the possible matches to the search string with in the text
/// </summary>
/// <param name="matches"> 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</param>
/// <returns>a list of the possible combinations that match the search text</returns>
internal static List<List<int>> GetAllMatchIndexes(bool[,] matches)
{
ArgumentNullException.ThrowIfNull(matches);

List<List<int>> results = new List<List<int>>();
// 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;
}
}

for (int secondIndex = 0; secondIndex < matches.GetLength(1); secondIndex++)
// Dynamic programming step: extend matches for the remaining characters s[1..m-1].
for (int k = 1; k < m; k++)
{
for (int firstIndex = 0; firstIndex < matches.GetLength(0); firstIndex++)
int currentMaxStart = -1;
int currentParentIndex = -1;

for (int i = 0; i < n; i++)
{
if (secondIndex == 0 && matches[firstIndex, secondIndex])
// 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])
{
results.Add(new List<int> { firstIndex });
if (currentMaxStart != -1)
{
bestStart[k, i] = currentMaxStart;
parent[k, i] = currentParentIndex;
}
}
else if (matches[firstIndex, secondIndex])

// 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)
{
var tempList = results.Where(x => x.Count == secondIndex && x[x.Count - 1] < firstIndex).Select(x => x.ToList()).ToList();
currentMaxStart = bestStart[k - 1, i];
currentParentIndex = i;
}
}
}

foreach (var pathSofar in tempList)
{
pathSofar.Add(firstIndex);
}
// 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);

results.AddRange(tempList);
if (score > maxScore)
{
maxScore = score;
bestEndIndex = i;
}
}
}

if (bestEndIndex == -1)
{
return [];
}

results = results.Where(x => x.Count == secondIndex + 1).ToList();
// Reconstruct only the winning path.
var result = new List<int>(m);
int curr = bestEndIndex;

for (int k = m - 1; k >= 0; k--)
{
result.Add(curr);
curr = parent[k, curr];
}

return results.Where(x => x.Count == matches.GetLength(1)).ToList();
result.Reverse();
return result;
}

/// <summary>
Expand Down