diff --git a/Wox.Infrastructure/StringMatcher.cs b/Wox.Infrastructure/StringMatcher.cs index 0ef4f1426..c619bc9a8 100644 --- a/Wox.Infrastructure/StringMatcher.cs +++ b/Wox.Infrastructure/StringMatcher.cs @@ -1,20 +1,21 @@ using System; -using System.Collections.Generic; using System.Linq; -using Wox.Infrastructure; +using System.Text; using Wox.Infrastructure.Logger; +using Wox.Infrastructure.UserSettings; namespace Wox.Infrastructure { public static class StringMatcher { + public static string UserSettingSearchPrecision { get; set; } + + [Obsolete("This method is obsolete and should not be used. Please use the static function StringMatcher.FuzzySearch")] public static int Score(string source, string target) { if (!string.IsNullOrEmpty(source) && !string.IsNullOrEmpty(target)) { - FuzzyMatcher matcher = FuzzyMatcher.Create(target); - var score = matcher.Evaluate(source).Score; - return score; + return FuzzySearch(target, source, new MatchOption()).Score; } else { @@ -22,6 +23,97 @@ namespace Wox.Infrastructure } } + [Obsolete("This method is obsolete and should not be used. Please use the static function StringMatcher.FuzzySearch")] + public static bool IsMatch(string source, string target) + { + return FuzzySearch(target, source, new MatchOption()).Score > 0; + } + + public enum SearchPrecisionScore + { + Regular = 50, + Low = 20, + None = 0 + } + + /// + /// refer to https://github.com/mattyork/fuzzy + /// + public static MatchResult FuzzySearch(string query, string stringToCompare, MatchOption opt) + { + if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) return new MatchResult { Success = false }; + + query.Trim(); + + var len = stringToCompare.Length; + var compareString = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare; + var pattern = opt.IgnoreCase ? query.ToLower() : query; + + var sb = new StringBuilder(stringToCompare.Length + (query.Length * (opt.Prefix.Length + opt.Suffix.Length))); + var patternIdx = 0; + var firstMatchIndex = -1; + var lastMatchIndex = 0; + char ch; + for (var idx = 0; idx < len; idx++) + { + ch = stringToCompare[idx]; + if (compareString[idx] == pattern[patternIdx]) + { + if (firstMatchIndex < 0) + firstMatchIndex = idx; + lastMatchIndex = idx + 1; + + sb.Append(opt.Prefix + ch + opt.Suffix); + patternIdx += 1; + } + else + { + sb.Append(ch); + } + + // match success, append remain char + if (patternIdx == pattern.Length && (idx + 1) != compareString.Length) + { + sb.Append(stringToCompare.Substring(idx + 1)); + break; + } + } + + // return rendered string if we have a match for every char + if (patternIdx == pattern.Length) + { + return new MatchResult + { + Success = true, + Value = sb.ToString(), + Score = CalScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex) + }; + } + + return new MatchResult { Success = false }; + } + + private static int CalScore(string query, string stringToCompare, int firstIndex, int matchLen) + { + //a match found near the beginning of a string is scored more than a match found near the end + //a match is scored more if the characters in the patterns are closer to each other, while the score is lower if they are more spread out + var score = 100 * (query.Length + 1) / ((1 + firstIndex) + (matchLen + 1)); + //a match with less characters assigning more weights + if (stringToCompare.Length - query.Length < 5) + score = score + 20; + else if (stringToCompare.Length - query.Length < 10) + score = score + 10; + + return score; + } + + public static bool IsPreciciseMatch(this MatchResult matchResult) + { + var precisionScore = (SearchPrecisionScore)Enum.Parse(typeof(SearchPrecisionScore), + UserSettingSearchPrecision ?? SearchPrecisionScore.Regular.ToString()); + return matchResult.Score >= (int)precisionScore; + } + public static int ScoreForPinyin(string source, string target) { if (!string.IsNullOrEmpty(source) && !string.IsNullOrEmpty(target)) @@ -34,12 +126,12 @@ namespace Wox.Infrastructure if (Alphabet.ContainsChinese(source)) { - FuzzyMatcher matcher = FuzzyMatcher.Create(target); - var combination = Alphabet.PinyinComination(source); - var pinyinScore = combination.Select(pinyin => matcher.Evaluate(string.Join("", pinyin)).Score) + var combination = Alphabet.PinyinComination(source); + var pinyinScore = combination + .Select(pinyin => FuzzySearch(target, string.Join("", pinyin), new MatchOption()).Score) .Max(); - var acronymScore = combination.Select(Alphabet.Acronym) - .Select(pinyin => matcher.Evaluate(pinyin).Score) + var acronymScore = combination.Select(Alphabet.Acronym) + .Select(pinyin => FuzzySearch(target, pinyin, new MatchOption()).Score) .Max(); var score = Math.Max(pinyinScore, acronymScore); return score; @@ -53,11 +145,37 @@ namespace Wox.Infrastructure { return 0; } + } + } + + public class MatchResult + { + public bool Success { get; set; } + public int Score { get; set; } + /// + /// hightlight string + /// + public string Value { get; set; } + } + + public class MatchOption + { + public MatchOption() + { + Prefix = ""; + Suffix = ""; + IgnoreCase = true; } - public static bool IsMatch(string source, string target) - { - return Score(source, target) > 0; - } + /// + /// prefix of match char, use for hightlight + /// + public string Prefix { get; set; } + /// + /// suffix of match char, use for hightlight + /// + public string Suffix { get; set; } + + public bool IgnoreCase { get; set; } } }