Editor/Common.cs
using System;
namespace Crux.Editor.Common
{
// https://gist.github.com/CDillinger/2aa02128f840bdca90340ce08ee71bc2
public static class FuzzyMatcher
{
// Score constants — tune these for your use case
const int SequentialBonus = 15; // bonus for adjacent matches
const int SeparatorBonus = 30; // bonus if match occurs after a separator
const int CamelBonus = 30; // bonus if match is uppercase and prev is lower
const int FirstLetterBonus = 15; // bonus if the first letter is matched
const int LeadingLetterPenalty = -5; // penalty applied for every letter in str before the first match
const int MaxLeadingLetterPenalty = -15; // maximum penalty for leading letters
const int UnmatchedLetterPenalty = -1; // penalty for every letter that doesn't matter
const int MaxRecursion = 10;
const int MaxMatches = 256;
/// <summary>
/// Simple fuzzy match — no scoring. Returns true if each character in pattern
/// is found sequentially within str.
/// </summary>
public static bool FuzzyMatchSimple( string pattern, string str )
{
int patternIdx = 0;
int strIdx = 0;
while ( patternIdx < pattern.Length && strIdx < str.Length )
{
if ( char.ToLower( pattern[patternIdx] ) == char.ToLower( str[strIdx] ) )
patternIdx++;
strIdx++;
}
return patternIdx == pattern.Length;
}
/// <summary>
/// Scored fuzzy match. Performs exhaustive search via recursion to find
/// the match with the highest score. Returns true if pattern was matched.
/// </summary>
public static bool FuzzyMatch( string pattern, string str, out int outScore )
{
int recursionCount = 0;
int[] matches = new int[MaxMatches];
return FuzzyMatchRecursive( pattern, str, 0, 0, null, matches, MaxMatches, 0, ref recursionCount,
out outScore );
}
/// <summary>
/// Scored fuzzy match with match index output. The matches array will contain
/// the indices of matched characters in str.
/// </summary>
public static bool FuzzyMatch( string pattern, string str, out int outScore, out int[] matchIndices )
{
int recursionCount = 0;
int[] matches = new int[MaxMatches];
bool result = FuzzyMatchRecursive( pattern, str, 0, 0, null, matches, MaxMatches, 0, ref recursionCount,
out outScore );
if ( result )
{
int count = pattern.Length;
matchIndices = new int[count];
Array.Copy( matches, matchIndices, count );
}
else
{
matchIndices = Array.Empty<int>();
}
return result;
}
static bool FuzzyMatchRecursive(
string pattern,
string str,
int patternIndex,
int strIndex,
int[] srcMatches,
int[] matches,
int maxMatches,
int nextMatch,
ref int recursionCount,
out int outScore )
{
outScore = 0;
// Count recursions
recursionCount++;
if ( recursionCount >= MaxRecursion )
return false;
// Detect end of strings
if ( patternIndex == pattern.Length || strIndex == str.Length )
return false;
// Recursion params
bool recursiveMatch = false;
int[] bestRecursiveMatches = new int[MaxMatches];
int bestRecursiveScore = 0;
// Loop through pattern and str looking for a match
bool firstMatch = true;
while ( patternIndex < pattern.Length && strIndex < str.Length )
{
// Found match
if ( char.ToLower( pattern[patternIndex] ) == char.ToLower( str[strIndex] ) )
{
// Supplied matches buffer was too short
if ( nextMatch >= maxMatches )
return false;
// "Copy-on-Write" srcMatches into matches
if ( firstMatch && srcMatches != null )
{
Array.Copy( srcMatches, matches, nextMatch );
firstMatch = false;
}
// Recursive call that "skips" this match
int[] recursiveMatches = new int[MaxMatches];
if ( FuzzyMatchRecursive( pattern, str, patternIndex, strIndex + 1, matches, recursiveMatches,
MaxMatches, nextMatch, ref recursionCount, out int recursiveScore ) )
{
// Pick best recursive score
if ( !recursiveMatch || recursiveScore > bestRecursiveScore )
{
Array.Copy( recursiveMatches, bestRecursiveMatches, MaxMatches );
bestRecursiveScore = recursiveScore;
}
recursiveMatch = true;
}
// Advance
matches[nextMatch++] = strIndex;
patternIndex++;
}
strIndex++;
}
// Determine if full pattern was matched
bool matched = patternIndex == pattern.Length;
// Calculate score
if ( matched )
{
// Initialize score
outScore = 100;
// Apply leading letter penalty
int penalty = LeadingLetterPenalty * matches[0];
if ( penalty < MaxLeadingLetterPenalty )
penalty = MaxLeadingLetterPenalty;
outScore += penalty;
// Apply unmatched penalty
int unmatched = str.Length - nextMatch;
outScore += UnmatchedLetterPenalty * unmatched;
// Apply ordering bonuses
for ( int i = 0; i < nextMatch; i++ )
{
int currIdx = matches[i];
if ( i > 0 )
{
int prevIdx = matches[i - 1];
// Sequential
if ( currIdx == prevIdx + 1 )
outScore += SequentialBonus;
}
// Check for bonuses based on neighbor character value
if ( currIdx > 0 )
{
// Camel case
char neighbor = str[currIdx - 1];
char curr = str[currIdx];
if ( char.IsLower( neighbor ) && char.IsUpper( curr ) )
outScore += CamelBonus;
// Separator
if ( neighbor == '_' || neighbor == ' ' || neighbor == '/' || neighbor == '\\' ||
neighbor == '.' )
outScore += SeparatorBonus;
}
else
{
// First letter
outScore += FirstLetterBonus;
}
}
}
// Return best result
if ( recursiveMatch && (!matched || bestRecursiveScore > outScore) )
{
// Recursive score is better than "this"
Array.Copy( bestRecursiveMatches, matches, maxMatches );
outScore = bestRecursiveScore;
return true;
}
else if ( matched )
{
// "this" score is better than recursive
return true;
}
return false;
}
}
}