diff --git a/Flow.Launcher.Infrastructure/IAlphabet.cs b/Flow.Launcher.Infrastructure/IAlphabet.cs new file mode 100644 index 00000000000..e79ec0c6d6f --- /dev/null +++ b/Flow.Launcher.Infrastructure/IAlphabet.cs @@ -0,0 +1,22 @@ +namespace Flow.Launcher.Infrastructure +{ + /// + /// Translate a language to English letters using a given rule. + /// + public interface IAlphabet + { + /// + /// Translate a string to English letters, using a given rule. + /// + /// String to translate. + /// + public (string translation, TranslationMapping map) Translate(string stringToTranslate); + + /// + /// Determine if a string can be translated to English letter with this Alphabet. + /// + /// String to translate. + /// + public bool ShouldTranslate(string stringToTranslate); + } +} diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 8eaa757bec1..1637a285c3f 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -1,209 +1,207 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; +using System.Collections.ObjectModel; using System.Text; -using JetBrains.Annotations; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; using ToolGood.Words.Pinyin; -using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Infrastructure { - public class TranslationMapping + public class PinyinAlphabet : IAlphabet { - private bool constructed; + private readonly ConcurrentDictionary _pinyinCache = + new(); - private List originalIndexs = new List(); - private List translatedIndexs = new List(); - private int translatedLength = 0; + private readonly Settings _settings; - public string key { get; private set; } - - public void setKey(string key) + public PinyinAlphabet() { - this.key = key; + _settings = Ioc.Default.GetRequiredService(); } - public void AddNewIndex(int originalIndex, int translatedIndex, int length) + public bool ShouldTranslate(string stringToTranslate) { - if (constructed) - throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); - - originalIndexs.Add(originalIndex); - translatedIndexs.Add(translatedIndex); - translatedIndexs.Add(translatedIndex + length); - translatedLength += length - 1; + return _settings.UseDoublePinyin ? + (!WordsHelper.HasChinese(stringToTranslate) && stringToTranslate.Length % 2 == 0) : + !WordsHelper.HasChinese(stringToTranslate); } - public int MapToOriginalIndex(int translatedIndex) + public (string translation, TranslationMapping map) Translate(string content) { - if (translatedIndex > translatedIndexs.Last()) - return translatedIndex - translatedLength - 1; - - int lowerBound = 0; - int upperBound = originalIndexs.Count - 1; - - int count = 0; - - // Corner case handle - if (translatedIndex < translatedIndexs[0]) - return translatedIndex; - if (translatedIndex > translatedIndexs.Last()) + if (_settings.ShouldUsePinyin) { - int indexDef = 0; - for (int k = 0; k < originalIndexs.Count; k++) + if (!_pinyinCache.TryGetValue(content, out var value)) + { + return BuildCacheFromContent(content); + } + else { - indexDef += translatedIndexs[k * 2 + 1] - translatedIndexs[k * 2]; + return value; } + } + return (content, null); + } - return translatedIndex - indexDef - 1; + private (string translation, TranslationMapping map) BuildCacheFromContent(string content) + { + if (!WordsHelper.HasChinese(content)) + { + return (content, null); } - // Binary Search with Range - for (int i = originalIndexs.Count / 2;; count++) + var resultList = WordsHelper.GetPinyinList(content); + + var resultBuilder = new StringBuilder(); + var map = new TranslationMapping(); + + var pre = false; + + for (var i = 0; i < resultList.Length; i++) { - if (translatedIndex < translatedIndexs[i * 2]) - { - // move to lower middle - upperBound = i; - i = (i + lowerBound) / 2; - } - else if (translatedIndex > translatedIndexs[i * 2 + 1] - 1) + if (content[i] >= 0x3400 && content[i] <= 0x9FD5) { - lowerBound = i; - // move to upper middle - // due to floor of integer division, move one up on corner case - i = (i + upperBound + 1) / 2; + string dp = _settings.UseDoublePinyin ? ToDoublePin(resultList[i]) : resultList[i]; + map.AddNewIndex(i, resultBuilder.Length, dp.Length + 1); + resultBuilder.Append(' '); + resultBuilder.Append(dp); + pre = true; } else - return originalIndexs[i]; - - if (upperBound - lowerBound <= 1 && - translatedIndex > translatedIndexs[lowerBound * 2 + 1] && - translatedIndex < translatedIndexs[upperBound * 2]) { - int indexDef = 0; - - for (int j = 0; j < upperBound; j++) + if (pre) { - indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2]; + pre = false; + resultBuilder.Append(' '); } - return translatedIndex - indexDef - 1; + resultBuilder.Append(resultList[i]); } } - } - - public void endConstruct() - { - if (constructed) - throw new InvalidOperationException("Mapping has already been constructed"); - constructed = true; - } - } - - /// - /// Translate a language to English letters using a given rule. - /// - public interface IAlphabet - { - /// - /// Translate a string to English letters, using a given rule. - /// - /// String to translate. - /// - public (string translation, TranslationMapping map) Translate(string stringToTranslate); - - /// - /// Determine if a string can be translated to English letter with this Alphabet. - /// - /// String to translate. - /// - public bool CanBeTranslated(string stringToTranslate); - } - public class PinyinAlphabet : IAlphabet - { - private ConcurrentDictionary _pinyinCache = - new ConcurrentDictionary(); + map.endConstruct(); - private Settings _settings; + var key = resultBuilder.ToString(); - public PinyinAlphabet() - { - Initialize(Ioc.Default.GetRequiredService()); + return _pinyinCache[content] = (key, map); } - private void Initialize([NotNull] Settings settings) + #region Double Pinyin + + private static readonly ReadOnlyDictionary special = new(new Dictionary(){ + {"A", "aa"}, + {"Ai", "ai"}, + {"An", "an"}, + {"Ang", "ah"}, + {"Ao", "ao"}, + {"E", "ee"}, + {"Ei", "ei"}, + {"En", "en"}, + {"Er", "er"}, + {"O", "oo"}, + {"Ou", "ou"} + }); + + private static readonly ReadOnlyDictionary first = new(new Dictionary(){ + {"Ch", "i"}, + {"Sh", "u"}, + {"Zh", "v"} + }); + + private static readonly ReadOnlyDictionary second = new(new Dictionary() { - _settings = settings ?? throw new ArgumentNullException(nameof(settings)); - } - - public bool CanBeTranslated(string stringToTranslate) + {"ua", "x"}, + {"ei", "w"}, + {"e", "e"}, + {"ou", "z"}, + {"iu", "q"}, + {"ve", "t"}, + {"ue", "t"}, + {"u", "u"}, + {"i", "i"}, + {"o", "o"}, + {"uo", "o"}, + {"ie", "p"}, + {"a", "a"}, + {"ong", "s"}, + {"iong", "s"}, + {"ai", "d"}, + {"ing", "k"}, + {"uai", "k"}, + {"ang", "h"}, + {"uan", "r"}, + {"an", "j"}, + {"en", "f"}, + {"ia", "x"}, + {"iang", "l"}, + {"uang", "l"}, + {"eng", "g"}, + {"in", "b"}, + {"ao", "c"}, + {"v", "v"}, + {"ui", "v"}, + {"un", "y"}, + {"iao", "n"}, + {"ian", "m"} + }); + + private static string ToDoublePin(string fullPinyin) { - return WordsHelper.HasChinese(stringToTranslate); - } + // Assuming s is valid + var fullPinyinSpan = fullPinyin.AsSpan(); + var doublePin = new StringBuilder(); - public (string translation, TranslationMapping map) Translate(string content) - { - if (_settings.ShouldUsePinyin) + // Handle special cases (a, o, e) + if (fullPinyin.Length <= 3 && (fullPinyinSpan[0] == 'a' || fullPinyinSpan[0] == 'e' || fullPinyinSpan[0] == 'o')) { - if (!_pinyinCache.ContainsKey(content)) - { - return BuildCacheFromContent(content); - } - else + if (special.TryGetValue(fullPinyin, out var value)) { - return _pinyinCache[content]; + return value; } } - return (content, null); - } - private (string translation, TranslationMapping map) BuildCacheFromContent(string content) - { - if (WordsHelper.HasChinese(content)) + // Check for initials that are two characters long (zh, ch, sh) + if (fullPinyin.Length >= 2) { - var resultList = WordsHelper.GetPinyinList(content); - - StringBuilder resultBuilder = new StringBuilder(); - TranslationMapping map = new TranslationMapping(); - - bool pre = false; - - for (int i = 0; i < resultList.Length; i++) + var firstTwo = fullPinyinSpan[..2]; + var firstTwoString = firstTwo.ToString(); + if (first.ContainsKey(firstTwoString)) { - if (content[i] >= 0x3400 && content[i] <= 0x9FD5) + doublePin.Append(firstTwoString); + + var lastTwo = fullPinyinSpan[2..]; + var lastTwoString = lastTwo.ToString(); + if (second.TryGetValue(lastTwoString, out var tmp)) { - map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1); - resultBuilder.Append(' '); - resultBuilder.Append(resultList[i]); - pre = true; + doublePin.Append(tmp); } else { - if (pre) - { - pre = false; - resultBuilder.Append(' '); - } - - resultBuilder.Append(resultList[i]); + doublePin.Append(lastTwo); } } - - map.endConstruct(); - - var key = resultBuilder.ToString(); - map.setKey(key); - - return _pinyinCache[content] = (key, map); } + // Handle single-character initials else { - return (content, null); + doublePin.Append(fullPinyinSpan[0]); + + var lastOne = fullPinyinSpan[1..]; + var lastOneString = lastOne.ToString(); + if (second.TryGetValue(lastOneString, out var tmp)) + { + doublePin.Append(tmp); + } + else + { + doublePin.Append(lastOne); + } } + + return doublePin.ToString(); } + + #endregion } } diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index e85c5d6f442..2882cb8f03e 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -68,7 +68,7 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption query = query.Trim(); TranslationMapping translationMapping = null; - if (_alphabet is not null && !_alphabet.CanBeTranslated(query)) + if (_alphabet is not null && _alphabet.ShouldTranslate(query)) { // We assume that if a query can be translated (containing characters of a language, like Chinese) // it actually means user doesn't want it to be translated to English letters. @@ -228,7 +228,7 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption return new MatchResult(false, UserSettingSearchPrecision); } - private bool IsAcronym(string stringToCompare, int compareStringIndex) + private static bool IsAcronym(string stringToCompare, int compareStringIndex) { if (IsAcronymChar(stringToCompare, compareStringIndex) || IsAcronymNumber(stringToCompare, compareStringIndex)) return true; @@ -237,7 +237,7 @@ private bool IsAcronym(string stringToCompare, int compareStringIndex) } // When counting acronyms, treat a set of numbers as one acronym ie. Visual 2019 as 2 acronyms instead of 5 - private bool IsAcronymCount(string stringToCompare, int compareStringIndex) + private static bool IsAcronymCount(string stringToCompare, int compareStringIndex) { if (IsAcronymChar(stringToCompare, compareStringIndex)) return true; diff --git a/Flow.Launcher.Infrastructure/TranslationMapping.cs b/Flow.Launcher.Infrastructure/TranslationMapping.cs new file mode 100644 index 00000000000..b33a094db89 --- /dev/null +++ b/Flow.Launcher.Infrastructure/TranslationMapping.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flow.Launcher.Infrastructure +{ + public class TranslationMapping + { + private bool constructed; + + private readonly List originalIndexes = new(); + private readonly List translatedIndexes = new(); + + private int translatedLength = 0; + + public void AddNewIndex(int originalIndex, int translatedIndex, int length) + { + if (constructed) + throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); + + originalIndexes.Add(originalIndex); + translatedIndexes.Add(translatedIndex); + translatedIndexes.Add(translatedIndex + length); + translatedLength += length - 1; + } + + public int MapToOriginalIndex(int translatedIndex) + { + if (translatedIndex > translatedIndexes.Last()) + return translatedIndex - translatedLength - 1; + + int lowerBound = 0; + int upperBound = originalIndexes.Count - 1; + + int count = 0; + + // Corner case handle + if (translatedIndex < translatedIndexes[0]) + return translatedIndex; + + if (translatedIndex > translatedIndexes.Last()) + { + int indexDef = 0; + for (int k = 0; k < originalIndexes.Count; k++) + { + indexDef += translatedIndexes[k * 2 + 1] - translatedIndexes[k * 2]; + } + + return translatedIndex - indexDef - 1; + } + + // Binary Search with Range + for (int i = originalIndexes.Count / 2;; count++) + { + if (translatedIndex < translatedIndexes[i * 2]) + { + // move to lower middle + upperBound = i; + i = (i + lowerBound) / 2; + } + else if (translatedIndex > translatedIndexes[i * 2 + 1] - 1) + { + lowerBound = i; + // move to upper middle + // due to floor of integer division, move one up on corner case + i = (i + upperBound + 1) / 2; + } + else + { + return originalIndexes[i]; + } + + if (upperBound - lowerBound <= 1 && + translatedIndex > translatedIndexes[lowerBound * 2 + 1] && + translatedIndex < translatedIndexes[upperBound * 2]) + { + int indexDef = 0; + + for (int j = 0; j < upperBound; j++) + { + indexDef += translatedIndexes[j * 2 + 1] - translatedIndexes[j * 2]; + } + + return translatedIndex - indexDef - 1; + } + } + } + + public void endConstruct() + { + if (constructed) + throw new InvalidOperationException("Mapping has already been constructed"); + constructed = true; + } + } +} diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index e304a1b5040..243c549c504 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -237,6 +237,8 @@ public CustomBrowserViewModel CustomBrowser /// public bool ShouldUsePinyin { get; set; } = false; + public bool UseDoublePinyin { get; set; } = true; //For developing + public bool AlwaysPreview { get; set; } = false; public bool AlwaysStartEn { get; set; } = false;