Shift+T 后面除了 rq / sj / xq 唤醒词,还可以直接写数字: - YYYYMMDD、YYYY/M/D、M/D(年份取今年)出那一天的日期格式,星期按那一天算 - YYYY/M 出年月格式(新增 date:ym_* 五种) - h:mm、h:mm:ss 出时间格式,写了秒才给带秒的格式 - YYYYMMDDhh[:mm[:ss]] 出日期加时间的格式 指定的日期时间沿用唤醒词的格式 ID,学到的顺序、置顶、固定位置跟着走。 输入形状放在 engine/contracts/date_time_input.h,TSF、Server、引擎共用: 数字、/ 和 : 只在还能接成某种形状时才进输入串,接不上的数字仍是选词键, / 和 : 仍是标点,Shift+数字始终选词。小键盘数字同样可以输入。 Refs #531
208 lines
7.4 KiB
C++
208 lines
7.4 KiB
C++
#pragma once
|
|
|
|
#include <cstddef>
|
|
|
|
// Shift+T 日期时间模式的输入形状。T 后面跟小写字母是唤醒词(rq / sj / xq,取此刻的日期时间),跟数字是
|
|
// 指定的日期时间:
|
|
//
|
|
// YYYYMMDD 日期 20241225
|
|
// YYYYMMDDhh[:mm[:ss]] 日期加时间,只写 hh 时分钟取 00 2024122514:30
|
|
// YYYY/M[M]/D[D] 日期 2024/12/25
|
|
// YYYY/M[M] 年月 2024/12
|
|
// M[M]/D[D] 今年的某月某日 12/25
|
|
// h[h]:mm[:ss] 时间 9:05、14:30:00
|
|
//
|
|
// 数字、/ 和 : 只在还能接成上面某一种形状时才是编码键:接不上的数字仍是选词键,接不上的 / 和 : 仍按标点
|
|
// 处理,Shift+数字始终选词。TSF 在同步吃键阶段按这里判断,Server 按同一条规则改输入串,引擎按它解析,三处
|
|
// 必须一致,否则两边的输入串会分叉。TSF 看不到配置,所以这里只认开头的 T,不管 T 模式开没开。
|
|
//
|
|
// 这里只看形状:月份、日期、时分秒的取值由引擎检查,取值不对时不出候选。
|
|
namespace FanyImeDateTimeInput
|
|
{
|
|
inline constexpr char kModePrefix = 'T';
|
|
// 最长的形状是 YYYYMMDDhh:mm:ss。
|
|
inline constexpr std::size_t kMaxBodyLength = 16;
|
|
inline constexpr std::size_t kMaxGroups = 3;
|
|
|
|
enum class Shape
|
|
{
|
|
None,
|
|
Date, // YYYYMMDD
|
|
DateTime, // YYYYMMDDhh[:mm[:ss]]
|
|
YearMonthDay, // YYYY/M/D
|
|
YearMonth, // YYYY/M
|
|
MonthDay, // M/D
|
|
Time, // h:mm[:ss]
|
|
};
|
|
|
|
// 被分隔符切开的一段数字:在 T 后面那一串里的起点和长度。
|
|
struct Group
|
|
{
|
|
std::size_t start = 0;
|
|
std::size_t length = 0;
|
|
};
|
|
|
|
struct Match
|
|
{
|
|
Shape shape = Shape::None;
|
|
std::size_t group_count = 0;
|
|
Group groups[kMaxGroups] = {};
|
|
};
|
|
|
|
namespace detail
|
|
{
|
|
template <typename Char> constexpr bool IsDigit(Char ch)
|
|
{
|
|
return ch >= Char('0') && ch <= Char('9');
|
|
}
|
|
|
|
struct Pattern
|
|
{
|
|
char separator; // 段与段之间的分隔符,只有一段时用不上
|
|
std::size_t max_groups; // 最多几段
|
|
std::size_t min_complete_groups; // 至少几段才算写完
|
|
std::size_t min_length[kMaxGroups]; // 每段至少几位
|
|
std::size_t max_length[kMaxGroups]; // 每段至多几位
|
|
Shape shape_by_groups[kMaxGroups]; // 写完时按段数给出的形状
|
|
};
|
|
|
|
// 顺序就是写完时的认定顺序:20241225 同时是 YYYYMMDD 写完和 YYYYMMDDhh 没写完,按前者算。
|
|
inline constexpr Pattern kPatterns[] = {
|
|
{'\0', 1, 1, {8, 0, 0}, {8, 0, 0}, {Shape::Date, Shape::None, Shape::None}},
|
|
{':', 3, 1, {10, 2, 2}, {10, 2, 2}, {Shape::DateTime, Shape::DateTime, Shape::DateTime}},
|
|
{'/', 3, 2, {4, 1, 1}, {4, 2, 2}, {Shape::None, Shape::YearMonth, Shape::YearMonthDay}},
|
|
{'/', 2, 2, {1, 1, 0}, {2, 2, 0}, {Shape::None, Shape::MonthDay, Shape::None}},
|
|
{':', 3, 2, {1, 2, 2}, {2, 2, 2}, {Shape::None, Shape::Time, Shape::Time}},
|
|
};
|
|
|
|
// 把 T 后面那一串切成数字段。只能含数字、/ 和 :,最多三段;有多个分隔符时必须是同一种。
|
|
template <typename Char>
|
|
constexpr bool Split(const Char *body, std::size_t size, Group (&groups)[kMaxGroups], std::size_t &count,
|
|
char &separator)
|
|
{
|
|
count = 1;
|
|
separator = '\0';
|
|
groups[0] = {0, 0};
|
|
for (std::size_t index = 0; index < size; ++index)
|
|
{
|
|
const Char ch = body[index];
|
|
if (IsDigit(ch))
|
|
{
|
|
++groups[count - 1].length;
|
|
continue;
|
|
}
|
|
if (ch != Char('/') && ch != Char(':'))
|
|
{
|
|
return false;
|
|
}
|
|
const char current = ch == Char('/') ? '/' : ':';
|
|
if ((separator != '\0' && current != separator) || count == kMaxGroups)
|
|
{
|
|
return false;
|
|
}
|
|
separator = current;
|
|
groups[count] = {index + 1, 0};
|
|
++count;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// complete 为 false 时只要求还能接成这个形状:最后一段可以没写完,紧跟在分隔符后面时甚至可以是空的。
|
|
constexpr bool Fits(const Pattern &pattern, const Group (&groups)[kMaxGroups], std::size_t count, char separator,
|
|
bool complete)
|
|
{
|
|
if (count > pattern.max_groups || (count > 1 && separator != pattern.separator) ||
|
|
(complete && count < pattern.min_complete_groups))
|
|
{
|
|
return false;
|
|
}
|
|
for (std::size_t index = 0; index < count; ++index)
|
|
{
|
|
const std::size_t length = groups[index].length;
|
|
const bool last = index + 1 == count;
|
|
if (length > pattern.max_length[index] || (length < pattern.min_length[index] && (complete || !last)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
} // namespace detail
|
|
|
|
// T 后面那一串是否以数字开头,即按指定日期时间而不是唤醒词处理。
|
|
template <typename Char> constexpr bool IsSpecificBody(const Char *body, std::size_t size)
|
|
{
|
|
return body != nullptr && size > 0 && detail::IsDigit(body[0]);
|
|
}
|
|
|
|
// T 后面那一串写完了时是哪种形状,各段在哪里;没写完或不是这些形状时 shape 为 None。
|
|
template <typename Char> constexpr Match MatchComplete(const Char *body, std::size_t size)
|
|
{
|
|
Match match;
|
|
char separator = '\0';
|
|
if (body == nullptr || size == 0 || size > kMaxBodyLength ||
|
|
!detail::Split(body, size, match.groups, match.group_count, separator))
|
|
{
|
|
return Match{};
|
|
}
|
|
for (const detail::Pattern &pattern : detail::kPatterns)
|
|
{
|
|
if (detail::Fits(pattern, match.groups, match.group_count, separator, true))
|
|
{
|
|
match.shape = pattern.shape_by_groups[match.group_count - 1];
|
|
return match;
|
|
}
|
|
}
|
|
return Match{};
|
|
}
|
|
|
|
// T 后面那一串是否还能接成某一种形状(空串也算:刚进 T 模式)。
|
|
template <typename Char> constexpr bool IsViablePrefix(const Char *body, std::size_t size)
|
|
{
|
|
if (size == 0)
|
|
{
|
|
return true;
|
|
}
|
|
Group groups[kMaxGroups] = {};
|
|
std::size_t count = 0;
|
|
char separator = '\0';
|
|
if (body == nullptr || size > kMaxBodyLength || !detail::Split(body, size, groups, count, separator))
|
|
{
|
|
return false;
|
|
}
|
|
for (const detail::Pattern &pattern : detail::kPatterns)
|
|
{
|
|
if (detail::Fits(pattern, groups, count, separator, false))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// 整个输入串 text(开头是 T)里,光标停在 text[caret] 时能否插入 ch(数字、/ 或 :)。插入后 T 后面那一串
|
|
// 还得能接成某一种形状,所以 Trq 后面不收数字,2024/12/25 后面也不再收。
|
|
template <typename Char> constexpr bool AcceptsAt(const Char *text, std::size_t size, std::size_t caret, Char ch)
|
|
{
|
|
if (text == nullptr || size == 0 || text[0] != Char(kModePrefix) || caret < 1 || caret > size ||
|
|
size > kMaxBodyLength || (!detail::IsDigit(ch) && ch != Char('/') && ch != Char(':')))
|
|
{
|
|
return false;
|
|
}
|
|
Char body[kMaxBodyLength] = {};
|
|
std::size_t length = 0;
|
|
for (std::size_t index = 1; index <= size; ++index)
|
|
{
|
|
if (index == caret)
|
|
{
|
|
body[length++] = ch;
|
|
}
|
|
if (index < size)
|
|
{
|
|
body[length++] = text[index];
|
|
}
|
|
}
|
|
return IsViablePrefix(body, length);
|
|
}
|
|
} // namespace FanyImeDateTimeInput
|