Skip to content

Commit

Permalink
Merge pull request #77 from Flow-Launcher/fix_stringmatcher_resultwei…
Browse files Browse the repository at this point in the history
…ghting

Fix StringMatcher's score calculation on distance of first match
  • Loading branch information
jjw24 authored Jun 28, 2020
2 parents 4c20dbc + 2ff4ffd commit 4676a2a
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 19 deletions.
35 changes: 30 additions & 5 deletions Flow.Launcher.Infrastructure/StringMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,18 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
bool allSubstringsContainedInCompareString = true;

var indexList = new List<int>();
List<int> spaceIndices = new List<int>();

for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++)
{

// To maintain a list of indices which correspond to spaces in the string to compare
// To populate the list only for the first query substring
if (fullStringToCompareWithoutCase[compareStringIndex].Equals(' ') && currentQuerySubstringIndex == 0)
{
spaceIndices.Add(compareStringIndex);
}

if (fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex])
{
matchFoundInPreviousLoop = false;
Expand Down Expand Up @@ -147,15 +156,31 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
// proceed to calculate score if every char or substring without whitespaces matched
if (allQuerySubstringsMatched)
{
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex);
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);

return new MatchResult(true, UserSettingSearchPrecision, indexList, score);
}

return new MatchResult (false, UserSettingSearchPrecision);
return new MatchResult(false, UserSettingSearchPrecision);
}

// 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
{
int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item)).Where(item => firstMatchIndex > item).FirstOrDefault();
int closestSpaceIndex = ind ?? -1;
return closestSpaceIndex;
}
}

private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
string fullStringToCompareWithoutCase, string currentQuerySubstring)
{
var allMatch = true;
Expand Down Expand Up @@ -299,13 +324,13 @@ private int ScoreAfterSearchPrecisionFilter(int rawScore)
public class MatchOption
{
/// <summary>
/// prefix of match char, use for hightlight
/// prefix of match char, use for highlight
/// </summary>
[Obsolete("this is never used")]
public string Prefix { get; set; } = "";

/// <summary>
/// suffix of match char, use for hightlight
/// suffix of match char, use for highlight
/// </summary>
[Obsolete("this is never used")]
public string Suffix { get; set; } = "";
Expand Down
88 changes: 74 additions & 14 deletions Flow.Launcher.Test/FuzzyMatcherTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public void MatchTest()
}

[TestCase("Chrome")]
public void WhenGivenNotAllCharactersFoundInSearchStringThenShouldReturnZeroScore(string searchString)
public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(string searchString)
{
var compareString = "Can have rum only in my glass";
var matcher = new StringMatcher();
Expand All @@ -92,7 +92,7 @@ public void WhenGivenNotAllCharactersFoundInSearchStringThenShouldReturnZeroScor
[TestCase("cand")]
[TestCase("cpywa")]
[TestCase("ccs")]
public void WhenGivenStringsAndAppliedPrecisionFilteringThenShouldReturnGreaterThanPrecisionScoreResults(string searchTerm)
public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreaterThanPrecisionScoreResults(string searchTerm)
{
var results = new List<Result>();
var matcher = new StringMatcher();
Expand All @@ -107,7 +107,10 @@ public void WhenGivenStringsAndAppliedPrecisionFilteringThenShouldReturnGreaterT

foreach (var precisionScore in GetPrecisionScores())
{
var filteredResult = results.Where(result => result.Score >= precisionScore).Select(result => result).OrderByDescending(x => x.Score).ToList();
var filteredResult = results.Where(result => result.Score >= precisionScore)
.Select(result => result)
.OrderByDescending(x => x.Score)
.ToList();

Debug.WriteLine("");
Debug.WriteLine("###############################################");
Expand All @@ -124,20 +127,22 @@ public void WhenGivenStringsAndAppliedPrecisionFilteringThenShouldReturnGreaterT
}

[TestCase(Chrome, Chrome, 157)]
[TestCase(Chrome, LastIsChrome, 103)]
[TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 21)]
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 15)]
[TestCase(Chrome, LastIsChrome, 147)]
[TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 25)]
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)]
[TestCase(Chrome, CandyCrushSagaFromKing, 0)]
[TestCase("sql", MicrosoftSqlServerManagementStudio, 56)]
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 99)]//double spacing intended
public void WhenGivenQueryStringThenShouldReturnCurrentScoring(string queryString, string compareString, int expectedScore)
[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 rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore;

// Should
Assert.AreEqual(expectedScore, rawScore, $"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}");
Assert.AreEqual(expectedScore, rawScore,
$"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}");
}

[TestCase("goo", "Google Chrome", StringMatcher.SearchPrecisionScore.Regular, true)]
Expand All @@ -150,7 +155,7 @@ public void WhenGivenQueryStringThenShouldReturnCurrentScoring(string queryStrin
[TestCase("ccs", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Low, true)]
[TestCase("cand", "Candy Crush Saga from King",StringMatcher.SearchPrecisionScore.Regular, true)]
[TestCase("cand", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)]
public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual(
public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual(
string queryString,
string compareString,
StringMatcher.SearchPrecisionScore expectedPrecisionScore,
Expand Down Expand Up @@ -185,8 +190,8 @@ public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual(
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
[TestCase("sql", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
[TestCase("sql serv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
[TestCase("sqlserv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
[TestCase("sql servman", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
[TestCase("servez", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
[TestCase("sql servz", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
[TestCase("sql serv man", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
[TestCase("sql studio", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
[TestCase("mic", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
Expand All @@ -199,7 +204,7 @@ public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual(
[TestCase("cod", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)]
[TestCase("code", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)]
[TestCase("codes", "Visual Studio Codes", StringMatcher.SearchPrecisionScore.Regular, true)]
public void WhenGivenQueryShouldReturnResultsContainingAllQuerySubstrings(
public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings(
string queryString,
string compareString,
StringMatcher.SearchPrecisionScore expectedPrecisionScore,
Expand All @@ -225,5 +230,60 @@ public void WhenGivenQueryShouldReturnResultsContainingAllQuerySubstrings(
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
$"Precision Score: {(int)expectedPrecisionScore}");
}

[TestCase("man", "Task Manager", "eManual")]
[TestCase("term", "Windows Terminal", "Character Map")]
[TestCase("winterm", "Windows Terminal", "Cygwin64 Terminal")]
public void WhenGivenAQuery_Scoring_ShouldGiveMoreWeightToStartOfNewWord(
string queryString, string compareString1, string compareString2)
{
// When
var matcher = new StringMatcher { UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular };

// Given
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
var compareString2Result = matcher.FuzzyMatch(queryString, compareString2);

Debug.WriteLine("");
Debug.WriteLine("###############################################");
Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}");
Debug.WriteLine($"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}");
Debug.WriteLine($"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
Debug.WriteLine("###############################################");
Debug.WriteLine("");

// Should
Assert.True(compareString1Result.Score > compareString2Result.Score,
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
$"Should be greater than{ Environment.NewLine}" +
$"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}");
}

[TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")]
public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore(
string queryString, string firstName, string firstDescription, string firstExecutableName,
string secondName, string secondDescription, string secondExecutableName)
{
// Act
var matcher = new StringMatcher();
var firstNameMatch = matcher.FuzzyMatch(queryString, firstName).RawScore;
var firstDescriptionMatch = matcher.FuzzyMatch(queryString, firstDescription).RawScore;
var firstExecutableNameMatch = matcher.FuzzyMatch(queryString, firstExecutableName).RawScore;

var secondNameMatch = matcher.FuzzyMatch(queryString, secondName).RawScore;
var secondDescriptionMatch = matcher.FuzzyMatch(queryString, secondDescription).RawScore;
var secondExecutableNameMatch = matcher.FuzzyMatch(queryString, secondExecutableName).RawScore;

var firstScore = new[] { firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch }.Max();
var secondScore = new[] { secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch }.Max();

// Assert
Assert.IsTrue(firstScore > secondScore,
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" +
$"Should be greater than{ Environment.NewLine}" +
$"Name of second: \"{secondName}\", Final Score: {secondScore}{Environment.NewLine}");
}
}
}

0 comments on commit 4676a2a

Please sign in to comment.