Files
HowGeGeandfanlusky 1671c3a467 fix(engine): recognize complete umlaut aliases in quanpin (#994)
* fix(engine): recognize complete umlaut aliases in quanpin

* refactor(engine): 移除九键输入

九键会话只有引擎公共 Session 门面在用,Server 与 TSF 都没有接入。删掉
NineKeySession、Session 的九键入口与 SessionSnapshot::nine_key_spellings,
以及对应测试和注释。

* fix(engine,server): ü 别名共用一条归一规则,入库与纠错不再产出查不到的键

- 新增 quanpin::is_jqxy_v_alias / canonical_umlaut_spelling,替换 canonical_syllable
  与 to_google_spelling 里各自手抄的别名表,normalize_umlaut_aliases 也改用它。
- 纠错表生成器与运行时生成式空间不再从 jqxy+v 别名派生纠错键(nvan -> jvan 之类),
  手误只纠到 u 的标准写法;重新生成 autocorrect_table.h。
- 个人词库新写入的拼音词、设置页导入的全拼按词库写法入库(xvan -> xuan、lue -> lve),
  否则查询归一后永远查不到;被替换的旧词保留原键,旧行仍可删除。
- 双拼音节集统一排除 jqxy+v 别名,round-trip 检查不再特判。

---------

Co-authored-by: fanlusky <1730976608@qq.com>
2026-10-16 20:48:33 +08:00

334 lines
8.7 KiB
C++

#include <algorithm>
#include <cctype>
#include <unordered_set>
#include <utf8.h>
#include <spdlog/spdlog.h>
#include <vector>
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/case_conv.hpp>
#include "shuangpin_utils.h"
#include "../core/data_path.h"
#include "../quanpin/quanpin_utils.h"
using namespace std;
const string ShuangpinUtil::app_name = "metasequoiaime";
string ShuangpinUtil::get_local_appdata_path()
{
return metasequoia::path_to_utf8(metasequoia::data_directory().parent_path());
}
namespace
{
std::string find_source_by_code(const std::unordered_map<std::string, std::string> &mapping, const std::string &code)
{
for (const auto &[source, mapped_code] : mapping)
{
if (mapped_code == code)
{
return source;
}
}
return {};
}
const std::unordered_set<std::string> &shuangpin_pinyin_set()
{
static const std::unordered_set<std::string> pinyin_set = [] {
auto result = quanpin::intact_pinyin_set();
// Preserve the historical exclusions that affect ambiguous shuangpin
// segmentation, but accept "yo" as the complete syllable for "哟".
result.insert("eng");
for (const char *pinyin : {"chua", "den", "fiao", "lo", "lue", "nou", "nue", "nun", "zhei"})
{
result.erase(pinyin);
}
// j/q/x/y 的 v 别名(jve、xvan……)解码器从不产出:cvt_single_sp_to_pinyin 把它们的 v 读成 u。
for (auto it = result.begin(); it != result.end();)
{
it = quanpin::is_jqxy_v_alias(*it) ? result.erase(it) : std::next(it);
}
return result;
}();
return pinyin_set;
}
} // namespace
/**
* @brief Convert one shuangpin syllable to quanpin using the selected profile
*
* Currently support 402 pinyin
*
* @param sp_str Shuangpin string
* @return string Quanpin string
*/
string ShuangpinUtil::cvt_single_sp_to_pinyin(string sp_str, const ShuangpinProfile &profile)
{
const std::string zero_initial = find_source_by_code(profile.zero_initials, sp_str);
if (!zero_initial.empty())
{
return zero_initial;
}
if (sp_str.size() != 2)
return "";
string res = "";
string sm;
vector<string> ym_list;
sm = find_source_by_code(profile.initials, sp_str.substr(0, 1));
if (sm.empty())
sm = sp_str.substr(0, 1);
for (const auto &pair : profile.finals)
{
if (pair.second == sp_str.substr(1, 1))
{
ym_list.push_back(pair.first);
}
}
if (sm == "" || ym_list.size() == 0)
{
return "";
}
for (const auto &ym : ym_list)
{
std::string normalized_ym = ym;
if (ym == "v" && (sm == "j" || sm == "q" || sm == "x" || sm == "y"))
{
normalized_ym = "u";
}
if (shuangpin_pinyin_set().count(sm + normalized_ym) > 0)
{
res = sm + normalized_ym;
}
}
return res;
}
bool ShuangpinUtil::is_accepted_syllable_code(const std::string &sp_str, const ShuangpinProfile &profile)
{
return shuangpin_pinyin_set().count(cvt_single_sp_to_pinyin(sp_str, profile)) > 0;
}
const std::unordered_set<std::string> &ShuangpinUtil::decodable_syllables()
{
return shuangpin_pinyin_set();
}
/**
* @brief Split shuangpin, using ' as delimiter, using forward greedy segmentation
*
* @param sp_str Shuangpin string
* @return string Segmented string with '
*/
string ShuangpinUtil::pinyin_segmentation(string sp_str, const ShuangpinProfile &profile)
{
if (sp_str.size() == 1)
{
return sp_str;
}
string res("");
string::size_type range_start = 0;
while (range_start < sp_str.size())
{
if ((range_start + 2) <= sp_str.size())
{
// Try to cut two chars to test
string cur_sp = sp_str.substr(range_start, 2);
if (is_accepted_syllable_code(boost::algorithm::to_lower_copy(cur_sp), profile))
{
res = res + "'" + cur_sp;
range_start += 2;
}
else
{
res = res + "'" + cur_sp.substr(0, 1);
range_start += 1;
}
}
else
{
res = res + "'" + sp_str.substr(sp_str.size() - 1, 1);
range_start += 1;
}
}
while (!res.empty() && res[0] == '\'')
{
res.erase(0, 1);
}
while (!res.empty() && res[res.size()] == '\'')
{
res.erase(res.size() - 1, 1);
}
return res;
}
/**
* @brief Get the first han char
*
* @param words
* @return std::string
*/
/**
* @brief Get first UTF-8 char size
*
* @param words UTF-8 string
* @return string::size_type Char size
*/
string::size_type ShuangpinUtil::get_first_char_size(string words)
{
size_t cplen = 1;
if ((words[0] & 0xf8) == 0xf0)
cplen = 4;
else if ((words[0] & 0xf0) == 0xe0)
cplen = 3;
else if ((words[0] & 0xe0) == 0xc0)
cplen = 2;
if (cplen > words.length())
cplen = 1;
return cplen;
}
/**
* @brief Count UTF-8 chars
*
* @param str
* @return string::size_type
*/
string::size_type ShuangpinUtil::count_utf8_chars(const string &str)
{
return utf8::distance(str.begin(), str.end());
}
/**
* @brief Extract preview without helpcodes
*
* @param candidate UTF-8 string
* @return string Pure hanzi string
*/
string ShuangpinUtil::extract_preview(string candidate)
{
size_t start_pos = candidate.find('(');
if (start_pos != string::npos)
{
return candidate.substr(0, start_pos);
}
return candidate;
}
/**
* @brief Check if all pinyin is quanpin
*
* @param pure_pinyin Pure shuangpin
* @param seg_pinyin Segmented shuangpin
* @return true If all pinyin is quanpin
* @return false Otherwise
*/
bool ShuangpinUtil::is_all_complete_pinyin(string pure_pinyin, string seg_pinyin)
{
if (pure_pinyin.size() % 2)
return false;
auto pinyin_size = seg_pinyin.size();
size_t index = 0;
while (index < pinyin_size)
{
if (seg_pinyin[index] == '\'' || seg_pinyin[index + 1] == '\'')
return false;
index += 3;
}
return true;
}
/**
* @brief Convert segmented shuangpin to segmented complete pinyin
*
* @param seg_shangpin Segmented shuangpin
* @return string Segmented quanpin
*/
string ShuangpinUtil::convert_seg_shuangpin_to_seg_complete_pinyin(string seg_shangpin, const ShuangpinProfile &profile)
{
vector<string> splitted_shuangpin;
boost::split(splitted_shuangpin, seg_shangpin, boost::is_any_of("'"));
string res = "";
for (auto each : splitted_shuangpin)
{
if (each.size() == 1)
{
const std::string initial = find_source_by_code(profile.initials, each);
res += (initial.empty() ? each : initial) + "'";
}
else if (each.size() == 2)
{
res += cvt_single_sp_to_pinyin(each, profile) + "'";
}
}
return res.substr(0, res.size() - 1);
}
/**
* @brief 判断是否是全码辅助
*
* @param pinyin
* @return true 长度为偶数,且末尾两个辅助码中至少一个是大写字母,且前面都是完整的双拼
* @return false
*/
bool ShuangpinUtil::IsFullHelpMode(std::string pinyin, const ShuangpinProfile &profile)
{
auto len = pinyin.size();
if (len == 0 || len == 2)
return false;
if (len % 2 != 0)
return false;
auto pure_pinyin = pinyin.substr(0, len - 2);
if (is_all_complete_pinyin(pure_pinyin, pinyin_segmentation(pure_pinyin, profile)))
{
const bool first_helpcode_is_upper = pinyin[len - 2] >= 'A' && pinyin[len - 2] <= 'Z';
const bool second_helpcode_is_upper = pinyin[len - 1] >= 'A' && pinyin[len - 1] <= 'Z';
if (first_helpcode_is_upper || second_helpcode_is_upper)
{
return true;
}
}
return false;
}
std::string ShuangpinUtil::GetFullHelpCodes(std::string pinyin)
{
if (pinyin.size() < 2)
{
return "";
}
const bool reverse = pinyin[pinyin.size() - 2] >= 'A' && pinyin[pinyin.size() - 2] <= 'Z';
std::string help_codes = pinyin.substr(pinyin.size() - 2, 2);
std::transform(help_codes.begin(), help_codes.end(), help_codes.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (reverse)
{
std::swap(help_codes[0], help_codes[1]);
}
return help_codes;
}
namespace shuangpin
{
std::string get_local_appdata_path()
{
return ShuangpinUtil::get_local_appdata_path();
}
std::string get_app_name()
{
return ShuangpinUtil::app_name;
}
std::filesystem::path get_data_file_path(const std::filesystem::path &relative_path)
{
return metasequoia::data_file_path(relative_path);
}
} // namespace shuangpin