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;