Files
Lu Fan(SonnyCalcr) 3c79876205 Merge pull request #629 from fanlusky/feat/v-mode
feat: 新增 V 模式,数字转中文与算式计算
2026-10-05 17:51:21 +08:00

355 lines
12 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "v_mode_query.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace metasequoia::local_modes
{
namespace
{
constexpr const char *kReadingDigits[] = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"};
constexpr const char *kFinancialDigits[] = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
// 逐位读数字用 〇,和日期里的「二〇二四年」一样。
constexpr const char *kSpelledDigits[] = {"〇", "一", "二", "三", "四", "五", "六", "七", "八", "九"};
constexpr const char *kReadingUnits[] = {"", "十", "百", "千"};
constexpr const char *kFinancialUnits[] = {"", "拾", "佰", "仟"};
constexpr const char *kGroupUnits[] = {"", "万", "亿", "万亿"};
// 万亿以上没有通用的读法,读法和大写都只到 16 位。
constexpr std::size_t kMaxIntegerDigits = 16;
// 括号嵌套的上限,防止病态输入把递归下降打爆栈。
constexpr int kMaxExpressionDepth = 64;
bool all_digits(const std::string &text)
{
return !text.empty() &&
std::all_of(text.begin(), text.end(), [](unsigned char ch) { return ch >= '0' && ch <= '9'; });
}
std::string strip_leading_zeros(const std::string &digits)
{
const std::size_t first = digits.find_first_not_of('0');
return first == std::string::npos ? std::string{} : digits.substr(first);
}
// 四位以内的一节,节内的零按「中间有零读一个零,末尾的零不读」处理。
std::string group_text(unsigned group, const char *const *digits, const char *const *units)
{
static constexpr unsigned kPowers[] = {1000, 100, 10, 1};
std::string text;
bool pending_zero = false;
for (int position = 0; position < 4; ++position)
{
const unsigned digit = group / kPowers[position] % 10;
if (digit == 0)
{
pending_zero = !text.empty();
continue;
}
if (pending_zero)
{
text += digits[0];
pending_zero = false;
}
text += digits[digit];
text += units[3 - position];
}
return text;
}
// 按四位一节从高到低拼,节与节之间:前面有数、这一节不满千或中间隔了全零的节,就补一个零。
std::string integer_text(const std::string &digits_text, bool financial)
{
const char *const *digits = financial ? kFinancialDigits : kReadingDigits;
const char *const *units = financial ? kFinancialUnits : kReadingUnits;
const std::string digits_only = strip_leading_zeros(digits_text);
if (digits_only.empty())
return digits[0];
if (digits_only.size() > kMaxIntegerDigits)
return {};
const std::size_t group_count = (digits_only.size() + 3) / 4;
const std::size_t head = digits_only.size() - (group_count - 1) * 4;
std::string text;
bool need_zero = false;
for (std::size_t index = 0; index < group_count; ++index)
{
const std::size_t start = index == 0 ? 0 : head + (index - 1) * 4;
const std::size_t length = index == 0 ? head : 4;
const unsigned group = static_cast<unsigned>(std::stoul(digits_only.substr(start, length)));
if (group == 0)
{
need_zero = true;
continue;
}
if (!text.empty() && (need_zero || group < 1000))
text += digits[0];
need_zero = false;
text += group_text(group, digits, units);
text += kGroupUnits[group_count - 1 - index];
}
// 读法里打头的「一十」说成「十」:十五、十万;大写照写「壹拾」。
if (!financial && text.rfind("一十", 0) == 0)
text.erase(0, std::strlen("一"));
return text;
}
std::string spelled_digits(const std::string &digits, const char *const *table)
{
std::string text;
for (const char digit : digits)
text += table[digit - '0'];
return text;
}
std::string with_thousands_separators(const std::string &digits)
{
std::string text;
for (std::size_t index = 0; index < digits.size(); ++index)
{
if (index > 0 && (digits.size() - index) % 3 == 0)
text += ',';
text += digits[index];
}
return text;
}
// 纯数字:整数,或「整数.小数」,各至少一位。
bool split_number(const std::string &input, std::string &integer, std::string &fraction)
{
const std::size_t dot = input.find('.');
integer = input.substr(0, dot);
fraction = dot == std::string::npos ? std::string{} : input.substr(dot + 1);
return all_digits(integer) && (dot == std::string::npos || all_digits(fraction));
}
std::vector<std::string> number_candidates(const std::string &integer, const std::string &fraction)
{
std::vector<std::string> texts;
const std::string reading = integer_text(integer, false);
const std::string financial = integer_text(integer, true);
if (fraction.empty())
{
const std::string digits_only = strip_leading_zeros(integer);
texts.push_back(reading);
texts.push_back(financial);
texts.push_back(chinese_money(integer, {}));
texts.push_back(spelled_digits(integer, kSpelledDigits));
if (digits_only.size() >= 4)
texts.push_back(with_thousands_separators(digits_only));
}
else
{
// 带小数时多半是金额,金额排第一。
texts.push_back(chinese_money(integer, fraction));
if (!reading.empty())
texts.push_back(reading + "点" + spelled_digits(fraction, kReadingDigits));
if (!financial.empty())
texts.push_back(financial + "点" + spelled_digits(fraction, kFinancialDigits));
}
return texts;
}
// + - 一层、* / 一层、一元正负号和括号,按通常的优先级和左结合求值。
class ExpressionParser
{
public:
explicit ExpressionParser(const std::string &text) : text_(text)
{
}
bool parse(double &value)
{
return expression(value, 0) && position_ == text_.size();
}
private:
bool expression(double &value, int depth)
{
if (!term(value, depth))
return false;
while (position_ < text_.size() && (text_[position_] == '+' || text_[position_] == '-'))
{
const char op = text_[position_++];
double right = 0;
if (!term(right, depth))
return false;
value = op == '+' ? value + right : value - right;
}
return true;
}
bool term(double &value, int depth)
{
if (!factor(value, depth))
return false;
while (position_ < text_.size() && (text_[position_] == '*' || text_[position_] == '/'))
{
const char op = text_[position_++];
double right = 0;
if (!factor(right, depth))
return false;
if (op == '/' && right == 0)
return false;
value = op == '*' ? value * right : value / right;
}
return true;
}
bool factor(double &value, int depth)
{
if (depth > kMaxExpressionDepth || position_ >= text_.size())
return false;
const char ch = text_[position_];
if (ch == '+' || ch == '-')
{
++position_;
if (!factor(value, depth + 1))
return false;
value = ch == '-' ? -value : value;
return true;
}
if (ch == '(')
{
++position_;
if (!expression(value, depth + 1) || position_ >= text_.size() || text_[position_] != ')')
return false;
++position_;
return true;
}
return number(value);
}
// 数字至少一位,小数点两边都要有数字:1.、.5、1.2.3 都不认。
bool number(double &value)
{
const std::size_t start = position_;
while (position_ < text_.size() && text_[position_] >= '0' && text_[position_] <= '9')
++position_;
if (position_ == start)
return false;
if (position_ < text_.size() && text_[position_] == '.')
{
const std::size_t fraction_start = ++position_;
while (position_ < text_.size() && text_[position_] >= '0' && text_[position_] <= '9')
++position_;
if (position_ == fraction_start)
return false;
}
value = std::strtod(text_.substr(start, position_ - start).c_str(), nullptr);
return true;
}
const std::string &text_;
std::size_t position_ = 0;
};
// 至多十位小数,去掉末尾的零:0.1+0.2 给 0.3,1/3 给 0.3333333333。太大的数用有效数字表示。
std::string format_result(double value)
{
if (!std::isfinite(value))
return {};
char buffer[64] = {};
if (std::fabs(value) >= 1e15)
{
std::snprintf(buffer, sizeof(buffer), "%.15g", value);
return buffer;
}
std::snprintf(buffer, sizeof(buffer), "%.10f", value);
std::string text = buffer;
text.erase(text.find_last_not_of('0') + 1);
if (!text.empty() && text.back() == '.')
text.pop_back();
return text == "-0" ? "0" : text;
}
bool has_operator(const std::string &input)
{
return input.find_first_of("+-*/()") != std::string::npos;
}
} // namespace
std::string chinese_number_reading(const std::string &digits)
{
return all_digits(digits) ? integer_text(digits, false) : std::string{};
}
std::string chinese_financial_number(const std::string &digits)
{
return all_digits(digits) ? integer_text(digits, true) : std::string{};
}
std::string chinese_money(const std::string &integer_digits, const std::string &fraction_digits)
{
if (!all_digits(integer_digits) || fraction_digits.size() > 2 ||
(!fraction_digits.empty() && !all_digits(fraction_digits)))
return {};
const std::string integer = integer_text(integer_digits, true);
if (integer.empty())
return {};
const bool integer_zero = strip_leading_zeros(integer_digits).empty();
const unsigned jiao = fraction_digits.size() >= 1 ? static_cast<unsigned>(fraction_digits[0] - '0') : 0;
const unsigned fen = fraction_digits.size() >= 2 ? static_cast<unsigned>(fraction_digits[1] - '0') : 0;
std::string text = integer_zero ? std::string{} : integer + "元";
if (jiao == 0 && fen == 0)
return integer_zero ? std::string(kFinancialDigits[0]) + "元整" : text + "整";
if (jiao > 0)
text += std::string(kFinancialDigits[jiao]) + "角";
else if (!integer_zero)
text += kFinancialDigits[0]; // 壹佰元零伍分
if (fen > 0)
text += std::string(kFinancialDigits[fen]) + "分";
return text;
}
bool evaluate_v_mode_expression(const std::string &expression, std::string &result)
{
double value = 0;
if (expression.empty() || !ExpressionParser(expression).parse(value))
return false;
result = format_result(value);
return !result.empty();
}
std::vector<WordItem> query_v_mode(const std::string &input, int limit)
{
if (limit <= 0 || input.empty())
return {};
std::vector<std::string> texts;
std::string integer;
std::string fraction;
if (!has_operator(input) && split_number(input, integer, fraction))
{
texts = number_candidates(integer, fraction);
}
else if (has_operator(input))
{
std::string result;
if (evaluate_v_mode_expression(input, result))
{
texts.push_back(result);
texts.push_back(input + "=" + result);
}
}
texts.erase(std::remove(texts.begin(), texts.end(), std::string{}), texts.end());
std::vector<WordItem> results;
for (std::string &text : texts)
{
if (std::find_if(results.begin(), results.end(), [&](const WordItem &item) { return item.word == text; }) !=
results.end())
continue;
if (static_cast<int>(results.size()) >= limit)
break;
results.emplace_back("", std::move(text), 0, CandidateSource::Generated);
}
for (std::size_t index = 0; index < results.size(); ++index)
results[index].weight = static_cast<std::int64_t>(results.size() - index);
return results;
}
} // namespace metasequoia::local_modes