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.
This commit is contained in:
张弘韬 2020-12-22 21:53:59 +08:00
parent 8a76ad000d
commit 59e61cebe3
2 changed files with 108 additions and 57 deletions

View file

@ -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<int> originalIndexs = new List<int>();
private List<int> translatedIndexs = new List<int>();
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<string, string> _pinyinCache = new ConcurrentDictionary<string, string>();
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
new ConcurrentDictionary<string, (string translation, TranslationMapping map)>();
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);
}
}
}

View file

@ -44,22 +44,12 @@ namespace Flow.Launcher.Infrastructure
/// </summary>
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<char>)),
// string s when s.Any(c => char.IsUpper(c)) && s.Any(c => char.IsLower(c)) =>
// (null, Regex.Split(s, @"(?<!^)(?=[A-Z])").Select(w => w.First())),
// _ => ((IEnumerable<char>)null, (IEnumerable<char>)null)
//};
TranslationMapping map;
(stringToCompare, map) = _alphabet?.Translate(stringToCompare) ?? (stringToCompare, null);
var currentQueryIndex = 0;
var acronymMatchData = new List<int>();
@ -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<int>();
List<int> spaceIndices = new List<int>();
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<int> GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, int firstMatchIndexInWord, List<int> indexList)
private static List<int> GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
int firstMatchIndexInWord, List<int> indexList)
{
var updatedList = new List<int>();
@ -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;
}
}
}