From 59e61cebe38f45918fc8df73882f8691430be564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=BC=98=E9=9F=AC?= Date: Tue, 22 Dec 2020 21:53:59 +0800 Subject: [PATCH] Add Mapping to original string after translation. Not sure about the performance, but seems satisfying. It requires at most n times loop (n: number of translated charater) mapping once. --- .../PinyinAlphabet.cs | 81 ++++++++++++++---- Flow.Launcher.Infrastructure/StringMatcher.cs | 84 +++++++++---------- 2 files changed, 108 insertions(+), 57 deletions(-) diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 80fd12820..4f1aedd4a 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -1,21 +1,77 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Text; using JetBrains.Annotations; using Flow.Launcher.Infrastructure.UserSettings; +using Microsoft.AspNetCore.Localization; using ToolGood.Words.Pinyin; namespace Flow.Launcher.Infrastructure { + public class TranslationMapping + { + private bool constructed; + + private List originalIndexs = new List(); + private List translatedIndexs = new List(); + private int translaedLength = 0; + + public void AddNewIndex(int originalIndex, int translatedIndex, int length) + { + if (constructed) + throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); + + originalIndexs.Add(originalIndex); + translatedIndexs.Add(translatedIndex); + translatedIndexs.Add(translatedIndex + length); + translaedLength += length - 1; + } + + public int? MapToOriginalIndex(int translatedIndex) + { + if (translatedIndex > translatedIndexs.Last()) + return translatedIndex - translaedLength - 1; + + for (var i = 0; i < originalIndexs.Count; i++) + { + if (translatedIndex >= translatedIndexs[i * 2] && translatedIndex < translatedIndexs[i * 2 + 1]) + return originalIndexs[i]; + if (translatedIndex < translatedIndexs[i * 2]) + { + int indexDiff = 0; + for (int j = 0; j < i; j++) + { + indexDiff += translatedIndexs[i * 2 + 1] - translatedIndexs[i * 2] - 1; + } + + return translatedIndex - indexDiff; + } + } + + return translatedIndex; + } + + public void endConstruct() + { + if (constructed) + throw new InvalidOperationException("Mapping has already been constructed"); + constructed = true; + } + } + public interface IAlphabet { - string Translate(string stringToTranslate); + public (string translation, TranslationMapping map) Translate(string stringToTranslate); } public class PinyinAlphabet : IAlphabet { - private ConcurrentDictionary _pinyinCache = new ConcurrentDictionary(); + private ConcurrentDictionary _pinyinCache = + new ConcurrentDictionary(); + + private Settings _settings; public void Initialize([NotNull] Settings settings) @@ -23,7 +79,7 @@ namespace Flow.Launcher.Infrastructure _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - public string Translate(string content) + public (string translation, TranslationMapping map) Translate(string content) { if (_settings.ShouldUsePinyin) { @@ -34,14 +90,7 @@ namespace Flow.Launcher.Infrastructure var resultList = WordsHelper.GetPinyinList(content); StringBuilder resultBuilder = new StringBuilder(); - - for (int i = 0; i < resultList.Length; i++) - { - if (content[i] >= 0x3400 && content[i] <= 0x9FD5) - resultBuilder.Append(resultList[i].First()); - } - - resultBuilder.Append(' '); + TranslationMapping map = new TranslationMapping(); bool pre = false; @@ -49,6 +98,7 @@ namespace Flow.Launcher.Infrastructure { if (content[i] >= 0x3400 && content[i] <= 0x9FD5) { + map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1); resultBuilder.Append(' '); resultBuilder.Append(resultList[i]); pre = true; @@ -60,15 +110,18 @@ namespace Flow.Launcher.Infrastructure pre = false; resultBuilder.Append(' '); } + resultBuilder.Append(resultList[i]); } } - return _pinyinCache[content] = resultBuilder.ToString(); + map.endConstruct(); + + return _pinyinCache[content] = (resultBuilder.ToString(), map); } else { - return content; + return (content, null); } } else @@ -78,7 +131,7 @@ namespace Flow.Launcher.Infrastructure } else { - return content; + return (content, null); } } } diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index 1a6ff47a7..e885798b7 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -44,22 +44,12 @@ namespace Flow.Launcher.Infrastructure /// public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt) { - if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) return new MatchResult(false, UserSettingSearchPrecision); + if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) + return new MatchResult(false, UserSettingSearchPrecision); query = query.Trim(); - - stringToCompare = _alphabet?.Translate(stringToCompare) ?? stringToCompare; - - // This also can be done by spliting the query - - //(var spaceSplit, var upperSplit) = stringToCompare switch - //{ - // string s when s.Contains(' ') => (s.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(w => w.First()), - // default(IEnumerable)), - // string s when s.Any(c => char.IsUpper(c)) && s.Any(c => char.IsLower(c)) => - // (null, Regex.Split(s, @"(? w.First())), - // _ => ((IEnumerable)null, (IEnumerable)null) - //}; + TranslationMapping map; + (stringToCompare, map) = _alphabet?.Translate(stringToCompare) ?? (stringToCompare, null); var currentQueryIndex = 0; var acronymMatchData = new List(); @@ -72,28 +62,24 @@ namespace Flow.Launcher.Infrastructure if (currentQueryIndex >= queryWithoutCase.Length) break; - if (compareIndex == 0 && queryWithoutCase[currentQueryIndex] == char.ToLower(stringToCompare[compareIndex])) - { - acronymMatchData.Add(compareIndex); - currentQueryIndex++; - continue; - } switch (stringToCompare[compareIndex]) { - case char c when compareIndex == 0 && queryWithoutCase[currentQueryIndex] == char.ToLower(stringToCompare[compareIndex]) - || (char.IsUpper(c) && char.ToLower(c) == queryWithoutCase[currentQueryIndex]) - || (char.IsWhiteSpace(c) && char.ToLower(stringToCompare[++compareIndex]) == queryWithoutCase[currentQueryIndex]) - || (char.IsNumber(c) && c == queryWithoutCase[currentQueryIndex]): - acronymMatchData.Add(compareIndex); + case var c when (compareIndex == 0 && queryWithoutCase[currentQueryIndex] == + char.ToLower(stringToCompare[compareIndex])) + || (char.IsUpper(c) && char.ToLower(c) == queryWithoutCase[currentQueryIndex]) + || (char.IsWhiteSpace(c) && char.ToLower(stringToCompare[++compareIndex]) == + queryWithoutCase[currentQueryIndex]) + || (char.IsNumber(c) && c == queryWithoutCase[currentQueryIndex]): + acronymMatchData.Add(map?.MapToOriginalIndex(compareIndex) ?? compareIndex); currentQueryIndex++; continue; - case char c when char.IsWhiteSpace(c): + case var c when char.IsWhiteSpace(c): compareIndex++; acronymScore -= 10; break; - case char c when char.IsUpper(c) || char.IsNumber(c): + case var c when char.IsUpper(c) || char.IsNumber(c): acronymScore -= 10; break; } @@ -105,7 +91,7 @@ namespace Flow.Launcher.Infrastructure var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare; - var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var querySubstrings = queryWithoutCase.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries); int currentQuerySubstringIndex = 0; var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; var currentQuerySubstringCharacterIndex = 0; @@ -120,9 +106,10 @@ namespace Flow.Launcher.Infrastructure var indexList = new List(); List spaceIndices = new List(); - for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++) + 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) @@ -130,7 +117,8 @@ namespace Flow.Launcher.Infrastructure spaceIndices.Add(compareStringIndex); } - if (fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex]) + if (fullStringToCompareWithoutCase[compareStringIndex] != + currentQuerySubstring[currentQuerySubstringCharacterIndex]) { matchFoundInPreviousLoop = false; continue; @@ -154,14 +142,16 @@ namespace Flow.Launcher.Infrastructure // in order to do so we need to verify all previous chars are part of the pattern var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex; - if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, fullStringToCompareWithoutCase, currentQuerySubstring)) + if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, + fullStringToCompareWithoutCase, currentQuerySubstring)) { matchFoundInPreviousLoop = true; // if it's the beginning character of the first query substring that is matched then we need to update start index firstMatchIndex = currentQuerySubstringIndex == 0 ? startIndexToVerify : firstMatchIndex; - indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, firstMatchIndexInWord, indexList); + indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, + firstMatchIndexInWord, indexList); } } @@ -174,11 +164,13 @@ namespace Flow.Launcher.Infrastructure if (currentQuerySubstringCharacterIndex == currentQuerySubstring.Length) { // if any of the substrings was not matched then consider as all are not matched - allSubstringsContainedInCompareString = matchFoundInPreviousLoop && allSubstringsContainedInCompareString; + allSubstringsContainedInCompareString = + matchFoundInPreviousLoop && allSubstringsContainedInCompareString; currentQuerySubstringIndex++; - allQuerySubstringsMatched = AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length); + allQuerySubstringsMatched = + AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length); if (allQuerySubstringsMatched) break; @@ -188,13 +180,16 @@ namespace Flow.Launcher.Infrastructure } } + // proceed to calculate score if every char or substring without whitespaces matched if (allQuerySubstringsMatched) { var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex); - var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); + var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, + lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); - return new MatchResult(true, UserSettingSearchPrecision, indexList, score); + var resultList = indexList.Distinct().Select(x => map?.MapToOriginalIndex(x) ?? x).ToList(); + return new MatchResult(true, UserSettingSearchPrecision, resultList, score); } return new MatchResult(false, UserSettingSearchPrecision); @@ -209,14 +204,15 @@ namespace Flow.Launcher.Infrastructure } else { - int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item)).Where(item => firstMatchIndex > item).FirstOrDefault(); + int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item)) + .FirstOrDefault(item => firstMatchIndex > item); int closestSpaceIndex = ind ?? -1; return closestSpaceIndex; } } private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex, - string fullStringToCompareWithoutCase, string currentQuerySubstring) + string fullStringToCompareWithoutCase, string currentQuerySubstring) { var allMatch = true; for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++) @@ -231,7 +227,8 @@ namespace Flow.Launcher.Infrastructure return allMatch; } - private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, int firstMatchIndexInWord, List indexList) + private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, + int firstMatchIndexInWord, List indexList) { var updatedList = new List(); @@ -252,7 +249,8 @@ namespace Flow.Launcher.Infrastructure return currentQuerySubstringIndex >= querySubstringsLength; } - private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, bool allSubstringsContainedInCompareString) + private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, + bool allSubstringsContainedInCompareString) { // 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, @@ -347,7 +345,7 @@ namespace Flow.Launcher.Infrastructure private bool IsSearchPrecisionScoreMet(int rawScore) { - return rawScore >= (int)SearchPrecision; + return rawScore >= (int) SearchPrecision; } private int ScoreAfterSearchPrecisionFilter(int rawScore) @@ -360,4 +358,4 @@ namespace Flow.Launcher.Infrastructure { public bool IgnoreCase { get; set; } = true; } -} +} \ No newline at end of file