Skip to content
Merged
Show file tree
Hide file tree
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
115 changes: 67 additions & 48 deletions Flow.Launcher.Infrastructure/StringMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
var currentAcronymQueryIndex = 0;
var acronymMatchData = new List<int>();

// preset acronymScore
int acronymScore = 100;
int acronymsTotalCount = 0;
int acronymsMatched = 0;

var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare;
var queryWithoutCase = opt.IgnoreCase ? query.ToLower() : query;

var querySubstrings = queryWithoutCase.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries);
var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
int currentQuerySubstringIndex = 0;
var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex];
var currentQuerySubstringCharacterIndex = 0;
Expand All @@ -87,12 +87,18 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
var indexList = new List<int>();
List<int> spaceIndices = new List<int>();

bool spaceMet = false;

for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++)
{
if (currentAcronymQueryIndex >= queryWithoutCase.Length
|| allQuerySubstringsMatched && acronymScore < (int) UserSettingSearchPrecision)
// If acronyms matching successfully finished, this gets the remaining not matched acronyms for score calculation
if (currentAcronymQueryIndex >= query.Length && acronymsMatched == query.Length)
{
if (IsAcronymCount(stringToCompare, compareStringIndex))
acronymsTotalCount++;
continue;
}

if (currentAcronymQueryIndex >= query.Length ||
currentAcronymQueryIndex >= query.Length && allQuerySubstringsMatched)
break;

// To maintain a list of indices which correspond to spaces in the string to compare
Expand All @@ -101,42 +107,21 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
spaceIndices.Add(compareStringIndex);

// Acronym check
if (char.IsUpper(stringToCompare[compareStringIndex]) ||
char.IsNumber(stringToCompare[compareStringIndex]) ||
char.IsWhiteSpace(stringToCompare[compareStringIndex]) ||
spaceMet)
if (IsAcronym(stringToCompare, compareStringIndex))
{
if (fullStringToCompareWithoutCase[compareStringIndex] ==
queryWithoutCase[currentAcronymQueryIndex])
{
if (!spaceMet)
{
char currentCompareChar = stringToCompare[compareStringIndex];
spaceMet = char.IsWhiteSpace(currentCompareChar);
// if is space, no need to check whether upper or digit, though insignificant
if (!spaceMet && compareStringIndex == 0 || char.IsUpper(currentCompareChar) ||
char.IsDigit(currentCompareChar))
{
acronymMatchData.Add(compareStringIndex);
}
}
else if (!(spaceMet = char.IsWhiteSpace(stringToCompare[compareStringIndex])))
{
acronymMatchData.Add(compareStringIndex);
}
acronymMatchData.Add(compareStringIndex);
acronymsMatched++;

currentAcronymQueryIndex++;
}
else
{
spaceMet = char.IsWhiteSpace(stringToCompare[compareStringIndex]);
// Acronym Penalty
if (!spaceMet)
{
acronymScore -= 10;
}
}
}

if (IsAcronymCount(stringToCompare, compareStringIndex))
acronymsTotalCount++;

// Acronym end

if (allQuerySubstringsMatched || fullStringToCompareWithoutCase[compareStringIndex] !=
Expand Down Expand Up @@ -204,11 +189,16 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
}
}

// return acronym Match if possible
if (acronymMatchData.Count == query.Length && acronymScore >= (int) UserSettingSearchPrecision)
// return acronym match if all query char matched
if (acronymsMatched > 0 && acronymsMatched == query.Length)
{
acronymMatchData = acronymMatchData.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList();
return new MatchResult(true, UserSettingSearchPrecision, acronymMatchData, acronymScore);
int acronymScore = acronymsMatched * 100 / acronymsTotalCount;

if (acronymScore >= (int)UserSettingSearchPrecision)
{
acronymMatchData = acronymMatchData.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList();
return new MatchResult(true, UserSettingSearchPrecision, acronymMatchData, acronymScore);
}
}

// proceed to calculate score if every char or substring without whitespaces matched
Expand All @@ -225,20 +215,49 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
return new MatchResult(false, UserSettingSearchPrecision);
}

private bool IsAcronym(string stringToCompare, int compareStringIndex)
{
if (IsAcronymChar(stringToCompare, compareStringIndex) || IsAcronymNumber(stringToCompare, compareStringIndex))
return true;

return false;
}

// When counting acronyms, treat a set of numbers as one acronym ie. Visual 2019 as 2 acronyms instead of 5
private bool IsAcronymCount(string stringToCompare, int compareStringIndex)
{
if (IsAcronymChar(stringToCompare, compareStringIndex))
return true;

if (IsAcronymNumber(stringToCompare, compareStringIndex))
return compareStringIndex == 0 || char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);


return false;
}

private bool IsAcronymChar(string stringToCompare, int compareStringIndex)
=> char.IsUpper(stringToCompare[compareStringIndex]) ||
compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym
char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);

private bool IsAcronymNumber(string stringToCompare, int compareStringIndex) => stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9;

// To get the index of the closest space which preceeds the first matching index
private int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
{
if (spaceIndices.Count == 0)
{
return -1;
}
else
var closestSpaceIndex = -1;

// spaceIndices should be ordered asc
foreach (var index in spaceIndices)
{
int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item))
.FirstOrDefault(item => firstMatchIndex > item);
int closestSpaceIndex = ind ?? -1;
return closestSpaceIndex;
if (index < firstMatchIndex)
closestSpaceIndex = index;
else
break;
}

return closestSpaceIndex;
}

private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
Expand Down
63 changes: 35 additions & 28 deletions Flow.Launcher.Test/FuzzyMatcherTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,17 @@ public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreat

[TestCase(Chrome, Chrome, 157)]
[TestCase(Chrome, LastIsChrome, 147)]
[TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 90)]
[TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)]
[TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)]
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)]
[TestCase(Chrome, CandyCrushSagaFromKing, 0)]
[TestCase("sql", MicrosoftSqlServerManagementStudio, 90)]
[TestCase("sql", MicrosoftSqlServerManagementStudio, 110)]
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended
public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring(
string queryString, string compareString, int expectedScore)
{
// When, Given
var matcher = new StringMatcher();
var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore;

// Should
Expand All @@ -151,13 +152,22 @@ public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring(
[TestCase("goo", "Google Chrome", SearchPrecisionScore.Regular, true)]
[TestCase("chr", "Google Chrome", SearchPrecisionScore.Low, true)]
[TestCase("chr", "Chrome", SearchPrecisionScore.Regular, true)]
[TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)]
[TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Low, true)]
[TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.Regular, false)]
[TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.None, true)]
[TestCase("ccs", "Candy Crush Saga from King", SearchPrecisionScore.Regular, true)]
[TestCase("ccs", "Candy Crush Saga from King", SearchPrecisionScore.Low, true)]
[TestCase("cand", "Candy Crush Saga from King", SearchPrecisionScore.Regular, true)]
[TestCase("cand", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular,
false)]
[TestCase("cand", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)]
[TestCase("vsc", VisualStudioCode, SearchPrecisionScore.Regular, true)]
[TestCase("vs", VisualStudioCode, SearchPrecisionScore.Regular, true)]
[TestCase("vc", VisualStudioCode, SearchPrecisionScore.Regular, true)]
[TestCase("vts", VisualStudioCode, SearchPrecisionScore.Regular, false)]
[TestCase("vcs", VisualStudioCode, SearchPrecisionScore.Regular, false)]
[TestCase("wt", "Windows Terminal From Microsoft Store", SearchPrecisionScore.Regular, false)]
[TestCase("vsp", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)]
[TestCase("vsp", "2019 Visual Studio Preview", SearchPrecisionScore.Regular, true)]
[TestCase("2019p", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)]
public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual(
string queryString,
string compareString,
Expand All @@ -180,18 +190,16 @@ public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual(

// Should
Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
$"Query:{queryString}{Environment.NewLine} " +
$"Compare:{compareString}{Environment.NewLine}" +
$"Query: {queryString}{Environment.NewLine} " +
$"Compare: {compareString}{Environment.NewLine}" +
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
$"Precision Score: {(int)expectedPrecisionScore}");
}

[TestCase("exce", "OverLeaf-Latex: An online LaTeX editor", SearchPrecisionScore.Regular, false)]
[TestCase("term", "Windows Terminal (Preview)", SearchPrecisionScore.Regular, true)]
[TestCase("sql s managa", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular,
false)]
[TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular,
false)]
[TestCase("sql s managa", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)]
[TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)]
[TestCase("sql s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
[TestCase("sql", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
Expand All @@ -204,18 +212,13 @@ public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual(
[TestCase("mssms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
[TestCase("msms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)]
[TestCase("chr", "Shutdown", SearchPrecisionScore.Regular, false)]
[TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).",
SearchPrecisionScore.Regular, false)]
[TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).",
SearchPrecisionScore.Regular, true)]
[TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, false)]
[TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, true)]
[TestCase("a test", "This is a test", SearchPrecisionScore.Regular, true)]
[TestCase("test", "This is a test", SearchPrecisionScore.Regular, true)]
[TestCase("cod", VisualStudioCode, SearchPrecisionScore.Regular, true)]
[TestCase("code", VisualStudioCode, SearchPrecisionScore.Regular, true)]
[TestCase("codes", "Visual Studio Codes", SearchPrecisionScore.Regular, true)]
[TestCase("vsc", VisualStudioCode, SearchPrecisionScore.Regular, true)]
[TestCase("vs", VisualStudioCode, SearchPrecisionScore.Regular, true)]
[TestCase("vc", VisualStudioCode, SearchPrecisionScore.Regular, true)]
public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings(
string queryString,
string compareString,
Expand Down Expand Up @@ -300,15 +303,19 @@ public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore(
$"Should be greater than{Environment.NewLine}" +
$"Name of second: \"{secondName}\", Final Score: {secondScore}{Environment.NewLine}");
}

[TestCase("vsc","Visual Studio Code", 100)]
[TestCase("jbr","JetBrain Rider",100)]
[TestCase("jr","JetBrain Rider",90)]
[TestCase("vs","Visual Studio",100)]
[TestCase("vs","Visual Studio Preview",100)]
[TestCase("vsp","Visual Studio Preview",100)]
[TestCase("vsp","Visual Studio",0)]
[TestCase("pc","Postman Canary",100)]

[TestCase("vsc", "Visual Studio Code", 100)]
[TestCase("jbr", "JetBrain Rider", 100)]
[TestCase("jr", "JetBrain Rider", 66)]
[TestCase("vs", "Visual Studio", 100)]
[TestCase("vs", "Visual Studio Preview", 66)]
[TestCase("vsp", "Visual Studio Preview", 100)]
[TestCase("pc", "postman canary", 100)]
[TestCase("psc", "Postman super canary", 100)]
[TestCase("psc", "Postman super Canary", 100)]
[TestCase("vsp", "Visual Studio", 0)]
[TestCase("vps", "Visual Studio", 0)]
[TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 75)]
public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString,
int desiredScore)
{
Expand Down
Loading