Skip to content

Double pinyin query #2427

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 45 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3abd05f
Implement double pinyin
VictoriousRaptor Nov 18, 2023
fb66353
Test double pinyin
VictoriousRaptor Nov 18, 2023
f6ae71a
Only convert to double pinyin when meeting Chinese
VictoriousRaptor Nov 18, 2023
e5285b1
Temp: compatibility with full pinyin option
VictoriousRaptor Nov 19, 2023
99ff3b2
Fix wrong condition
VictoriousRaptor Nov 20, 2023
46d49d8
Only translate when string is double pinyin
VictoriousRaptor May 25, 2024
b1cb852
Extract classes
VictoriousRaptor Jun 2, 2024
6807afb
Remove unused alphabet arg in PublicAPIInstance
VictoriousRaptor Jun 2, 2024
a2efa11
Merge DoublePinAlphabet logic
VictoriousRaptor Jun 2, 2024
12c4e37
Developing
VictoriousRaptor Jun 2, 2024
f673000
Fix ShouldTranslate()
VictoriousRaptor Jun 2, 2024
b10a6e1
Capitalize first letter
VictoriousRaptor Jun 2, 2024
b816d1b
Remove unused key in pinyin alphabet
VictoriousRaptor Jun 2, 2024
c8a9e5e
Merge branch 'dev' into double-pin
Jack251970 Apr 5, 2025
9e8a950
Fix build issue & Improve code quality
Jack251970 Apr 5, 2025
a8a305f
Improve code quality
Jack251970 Apr 5, 2025
5be732d
Use var when neccessary
Jack251970 Apr 5, 2025
1f458d3
Fix typos & Code quality
Jack251970 Apr 9, 2025
3f45c6a
Merge branch 'dev' into double-pin
Jack251970 Apr 9, 2025
4b7db3c
Make function static
Jack251970 Apr 10, 2025
f5fd6b5
Use ReadOnlySpan instead
Jack251970 Apr 10, 2025
2cf6f81
Merge branch 'dev' into double-pin
Jack251970 Apr 10, 2025
e07b33c
Merge branch 'dev' into double-pin
VictoriousRaptor Jun 9, 2025
672649c
Merge branch 'dev' into double-pin
VictoriousRaptor Jun 10, 2025
74d5499
Delete Flow.Launcher/Properties/Resources.fr-FR.resx
VictoriousRaptor Jun 12, 2025
78ffeb8
Delete Flow.Launcher/Properties/Resources.he-IL.resx
VictoriousRaptor Jun 12, 2025
3eb5fea
remove on tag deployment & change NuGet publish to on master push
jjw24 Jun 11, 2025
8f43de6
Support Msix FireFox bookmarks
Jack251970 Jun 6, 2025
281e042
Fix IsRelative logic.
Jack251970 Jun 6, 2025
ceb05e8
Add error handling for directory operation
Jack251970 Jun 6, 2025
aaa8e4d
Use AddRange
Jack251970 Jun 8, 2025
44304f2
Change code comments
Jack251970 Jun 11, 2025
16fd256
Fix typos
Jack251970 Jun 12, 2025
fba42ff
Fix transaltion logic
VictoriousRaptor Jun 13, 2025
b31a740
Simple refactor
VictoriousRaptor Jun 13, 2025
818aac7
Use lookup table to translate full pinyin to double pinyin
VictoriousRaptor Jun 14, 2025
31cd894
Compress json
VictoriousRaptor Jun 14, 2025
3c2581d
Merge branch 'dev' into double-pin
VictoriousRaptor Jun 14, 2025
4fb2e3d
Fix translation mapping logic
VictoriousRaptor Jun 14, 2025
4b6231b
Extract methods for readability
VictoriousRaptor Jun 14, 2025
64a3aa5
Fix Off-by-one in index mapping when consecutive Chinese chars
VictoriousRaptor Jun 19, 2025
b189595
Add OnPropertyChanged() for double pinyin properties
VictoriousRaptor Jun 19, 2025
d2dc307
Fix logic of ShouldTranslate()
VictoriousRaptor Jun 19, 2025
f064a81
Merge branch 'dev' into double-pin
VictoriousRaptor Jun 19, 2025
1bc80d5
Fix translated length
VictoriousRaptor Jun 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Flow.Launcher.Infrastructure/IAlphabet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Flow.Launcher.Infrastructure
{
/// <summary>
/// Translate a language to English letters using a given rule.
/// </summary>
public interface IAlphabet
{
/// <summary>
/// Translate a string to English letters, using a given rule.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public (string translation, TranslationMapping map) Translate(string stringToTranslate);

/// <summary>
/// Determine if a string can be translated to English letter with this Alphabet.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public bool ShouldTranslate(string stringToTranslate);
}
}
238 changes: 85 additions & 153 deletions Flow.Launcher.Infrastructure/PinyinAlphabet.cs
Original file line number Diff line number Diff line change
@@ -1,209 +1,141 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using System.IO;
using System.Text;
using JetBrains.Annotations;
using System.Text.Json;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings;
using ToolGood.Words.Pinyin;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;

namespace Flow.Launcher.Infrastructure
{
public class TranslationMapping
public class PinyinAlphabet : IAlphabet
{
private bool constructed;

private List<int> originalIndexs = new List<int>();
private List<int> translatedIndexs = new List<int>();
private int translatedLength = 0;

public string key { get; private set; }
private readonly ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
new();

public void setKey(string key)
{
this.key = key;
}
private readonly Settings _settings;

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);
translatedLength += length - 1;
}
private ReadOnlyDictionary<string, string> currentDoublePinyinTable;

public int MapToOriginalIndex(int translatedIndex)
public PinyinAlphabet()
{
if (translatedIndex > translatedIndexs.Last())
return translatedIndex - translatedLength - 1;

int lowerBound = 0;
int upperBound = originalIndexs.Count - 1;

int count = 0;
_settings = Ioc.Default.GetRequiredService<Settings>();
LoadDoublePinyinTable();

// Corner case handle
if (translatedIndex < translatedIndexs[0])
return translatedIndex;
if (translatedIndex > translatedIndexs.Last())
_settings.PropertyChanged += (sender, e) =>
{
int indexDef = 0;
for (int k = 0; k < originalIndexs.Count; k++)
if (e.PropertyName == nameof(Settings.UseDoublePinyin) ||
e.PropertyName == nameof(Settings.DoublePinyinSchema))
{
indexDef += translatedIndexs[k * 2 + 1] - translatedIndexs[k * 2];
LoadDoublePinyinTable();
_pinyinCache.Clear();
}
};
}

return translatedIndex - indexDef - 1;
}

// Binary Search with Range
for (int i = originalIndexs.Count / 2;; count++)
private void LoadDoublePinyinTable()
{
if (_settings.UseDoublePinyin)
{
if (translatedIndex < translatedIndexs[i * 2])
{
// move to lower middle
upperBound = i;
i = (i + lowerBound) / 2;
}
else if (translatedIndex > translatedIndexs[i * 2 + 1] - 1)
var tablePath = Path.Join(AppContext.BaseDirectory, "Resources", "double_pinyin.json");
try
{
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 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++)
using var fs = File.OpenRead(tablePath);
Dictionary<string, Dictionary<string, string>> table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(fs);
if (!table.TryGetValue(_settings.DoublePinyinSchema, out var value))
{
indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2];
throw new InvalidOperationException("DoublePinyinSchema is invalid.");
}

return translatedIndex - indexDef - 1;
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(value);
}
catch (System.Exception e)
{
Log.Exception(nameof(PinyinAlphabet), "Failed to load double pinyin table from file: " + tablePath, e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
}
else
{
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
}

public void endConstruct()
public bool ShouldTranslate(string stringToTranslate)
{
if (constructed)
throw new InvalidOperationException("Mapping has already been constructed");
constructed = true;
return _settings.UseDoublePinyin ?
(!WordsHelper.HasChinese(stringToTranslate) && stringToTranslate.Length % 2 == 0) :
!WordsHelper.HasChinese(stringToTranslate);
}
}

/// <summary>
/// Translate a language to English letters using a given rule.
/// </summary>
public interface IAlphabet
{
/// <summary>
/// Translate a string to English letters, using a given rule.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public (string translation, TranslationMapping map) Translate(string stringToTranslate);

/// <summary>
/// Determine if a string can be translated to English letter with this Alphabet.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public bool CanBeTranslated(string stringToTranslate);
}

public class PinyinAlphabet : IAlphabet
{
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
new ConcurrentDictionary<string, (string translation, TranslationMapping map)>();

private Settings _settings;

public PinyinAlphabet()
{
Initialize(Ioc.Default.GetRequiredService<Settings>());
}

private void Initialize([NotNull] Settings settings)
public (string translation, TranslationMapping map) Translate(string content)
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
if (!_settings.ShouldUsePinyin)
return (content, null);

public bool CanBeTranslated(string stringToTranslate)
{
return WordsHelper.HasChinese(stringToTranslate);
return _pinyinCache.TryGetValue(content, out var value)
? value
: BuildCacheFromContent(content);
}

public (string translation, TranslationMapping map) Translate(string content)
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
{
if (_settings.ShouldUsePinyin)
if (!WordsHelper.HasChinese(content))
{
if (!_pinyinCache.ContainsKey(content))
{
return BuildCacheFromContent(content);
}
else
{
return _pinyinCache[content];
}
return (content, null);
}
return (content, null);
}

private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
{
if (WordsHelper.HasChinese(content))
{
var resultList = WordsHelper.GetPinyinList(content);
var resultList = WordsHelper.GetPinyinList(content);

StringBuilder resultBuilder = new StringBuilder();
TranslationMapping map = new TranslationMapping();
var resultBuilder = new StringBuilder();
var map = new TranslationMapping();

bool pre = false;
var previousIsChinese = false;

for (int i = 0; i < resultList.Length; i++)
for (var i = 0; i < resultList.Length; i++)
{
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
{
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
string dp = _settings.UseDoublePinyin ? ToDoublePin(resultList[i]) : resultList[i];
map.AddNewIndex(i, resultBuilder.Length, dp.Length + 1);
if (previousIsChinese)
{
map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
resultBuilder.Append(' ');
resultBuilder.Append(resultList[i]);
pre = true;
}
else
resultBuilder.Append(dp);
}
else
{
if (previousIsChinese)
{
if (pre)
{
pre = false;
resultBuilder.Append(' ');
}

resultBuilder.Append(resultList[i]);
previousIsChinese = false;
resultBuilder.Append(' ');
}
resultBuilder.Append(resultList[i]);
}
}

map.endConstruct();
map.endConstruct();

var key = resultBuilder.ToString();
map.setKey(key);
var key = resultBuilder.ToString();

return _pinyinCache[content] = (key, map);
}
else
return _pinyinCache[content] = (key, map);
}

#region Double Pinyin

private string ToDoublePin(string fullPinyin)
{
if (currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue))
{
return (content, null);
return doublePinyinValue;
}
return fullPinyin;
}

#endregion
}
}
6 changes: 3 additions & 3 deletions Flow.Launcher.Infrastructure/StringMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

public static MatchResult FuzzySearch(string query, string stringToCompare)
{
return Ioc.Default.GetRequiredService<StringMatcher>().FuzzyMatch(query, stringToCompare);

Check warning on line 32 in Flow.Launcher.Infrastructure/StringMatcher.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Ioc` is not a recognized word. (unrecognized-spelling)
}

public MatchResult FuzzyMatch(string query, string stringToCompare)
Expand Down Expand Up @@ -68,7 +68,7 @@

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.
Expand Down Expand Up @@ -228,7 +228,7 @@
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;
Expand All @@ -237,7 +237,7 @@
}

// 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;
Expand Down
Loading
Loading