Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
92 changes: 92 additions & 0 deletions autotests/dfm-search-tests/tst_chinese_nlp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ private Q_SLOTS:
void timeCustom_month();
void timeCustom_yearMonth();
void timeCustom_yearMonth_separators();
void timeCustom_yearMonth_chineseYear();
void timeCustom_yearMonth_chineseYear_noResidualKeyword();
void timeCustom_date();
void timeCustom_dateSpoken();
void timeCustom_fullDate();
Expand Down Expand Up @@ -1045,6 +1047,96 @@ void tst_ChineseNLP::timeCustom_yearMonth_separators()
QCOMPARE(intent3.timeConstraint().customStart().date().month(), 12);
}

// BUG-372165: Chinese-numeral year ("二五年"/"二零二五年"/"二〇二五年") must be
// recognised by the year rules, not leaked into keywords / bound to the current year.
void tst_ChineseNLP::timeCustom_yearMonth_chineseYear()
{
// "二五年十二月" — 2-digit Chinese year + Chinese month → 2025-12
ParsedIntent intent1;
m_parser->parse(QStringLiteral("二五年十二月的文档"), intent1);
QCOMPARE(intent1.timeConstraint().kind(), TimeConstraintKind::Custom);
QCOMPARE(intent1.timeConstraint().customStart().date().year(), 2025);
QCOMPARE(intent1.timeConstraint().customStart().date().month(), 12);
QCOMPARE(intent1.timeConstraint().customStart().date().day(), 1);
QCOMPARE(intent1.timeConstraint().customEnd().date().year(), 2025);
QCOMPARE(intent1.timeConstraint().customEnd().date().month(), 12);

// "二五年12月" — 2-digit Chinese year + Arabic month → 2025-12
ParsedIntent intent2;
m_parser->parse(QStringLiteral("二五年12月的文档"), intent2);
QCOMPARE(intent2.timeConstraint().kind(), TimeConstraintKind::Custom);
QCOMPARE(intent2.timeConstraint().customStart().date().year(), 2025);
QCOMPARE(intent2.timeConstraint().customStart().date().month(), 12);

// "二零二五年十二月" — 4-digit Chinese year (with 零) + Chinese month → 2025-12
ParsedIntent intent3;
m_parser->parse(QStringLiteral("二零二五年十二月的文档"), intent3);
QCOMPARE(intent3.timeConstraint().kind(), TimeConstraintKind::Custom);
QCOMPARE(intent3.timeConstraint().customStart().date().year(), 2025);
QCOMPARE(intent3.timeConstraint().customStart().date().month(), 12);

// "二〇二五年十二月" — 4-digit Chinese year (with 〇) + Chinese month → 2025-12
ParsedIntent intent4;
m_parser->parse(QStringLiteral("二〇二五年十二月的文档"), intent4);
QCOMPARE(intent4.timeConstraint().kind(), TimeConstraintKind::Custom);
QCOMPARE(intent4.timeConstraint().customStart().date().year(), 2025);
QCOMPARE(intent4.timeConstraint().customStart().date().month(), 12);

// Regression: a bare Chinese-numeral year ("二零二五年") must be parsed as a year,
// spanning the whole year.
ParsedIntent intent5;
m_parser->parse(QStringLiteral("二零二五年的文档"), intent5);
QCOMPARE(intent5.timeConstraint().kind(), TimeConstraintKind::Custom);
QCOMPARE(intent5.timeConstraint().customStart().date().year(), 2025);
QCOMPARE(intent5.timeConstraint().customStart().date().month(), 1);
QCOMPARE(intent5.timeConstraint().customEnd().date().year(), 2025);
QCOMPARE(intent5.timeConstraint().customEnd().date().month(), 12);

// Regression: "十二月的文件" must NOT be mis-parsed as a year-month; it stays a
// month of the current year (the year capture group requires >= 2 Chinese digits
// and cannot start with 十).
ParsedIntent intent6;
m_parser->parse(QStringLiteral("十二月的文件"), intent6);
QCOMPARE(intent6.timeConstraint().kind(), TimeConstraintKind::Custom);
QCOMPARE(intent6.timeConstraint().customStart().date().month(), 12);
QCOMPARE(intent6.timeConstraint().customStart().date().year(), QDate::currentDate().year());
}

// BUG-372165 scenario: the full叠加 query "二五年十二月创建的表格" must produce a
// correct time constraint, the spreadsheet filetype, and NO residual keyword.
void tst_ChineseNLP::timeCustom_yearMonth_chineseYear_noResidualKeyword()
{
ParsedIntent intent;
m_parser->parse(QStringLiteral("二五年十二月创建的表格"), intent);

// Correct time constraint: the whole month of 2025-12, not the current year.
QCOMPARE(intent.timeConstraint().kind(), TimeConstraintKind::Custom);
QCOMPARE(intent.timeConstraint().customStart().date().year(), 2025);
QCOMPARE(intent.timeConstraint().customStart().date().month(), 12);
QCOMPARE(intent.timeConstraint().customEnd().date().year(), 2025);
QCOMPARE(intent.timeConstraint().customEnd().date().month(), 12);

// Spreadsheet filetype consumed.
QVERIFY(intent.fileExtensions().contains("xls"));
QVERIFY(intent.fileExtensions().contains("xlsx"));
QVERIFY(intent.fileExtensions().contains("csv"));

// The Chinese-numeral year must be fully consumed by a time rule, not leaked
// into keywords (which was the root cause of empty search results).
QVERIFY2(intent.keywords().isEmpty(),
"Chinese-numeral year must not leak into keywords");

// The consumed span must be the year-month rule, covering the whole "二五年十二月".
bool hasYearMonth = false;
for (const MatchSpan &span : intent.consumedSpans()) {
if (span.ruleId() == QLatin1String("time_exact_year_month")) {
hasYearMonth = true;
break;
}
}
QVERIFY(hasYearMonth);
}

void tst_ChineseNLP::timeCustom_date()
{
// "12月5日" → this year, Dec 5
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,27 @@
}
}

// Digit-by-digit Chinese numeral string (e.g., "二五" = 25, "二零二五" = 2025,
// "二〇二五" = 2025). Used for year written in Chinese numerals position-by-position.
// Each character must map to a single digit 0-9 — this excludes "十" (10) and "两",
// which are not used in digit-by-digit year notation. This branch runs after the
// "十"-based branches above, so "十一"/"二十"/"十二" are already handled.
bool allSingleDigits = true;
for (const QChar &ch : input) {
auto it = digitMap.constFind(ch);
if (it == digitMap.constEnd() || it.value() > 9) {
allSingleDigits = false;
break;
}
}
if (allSingleDigits && input.size() >= 2) {
int value = 0;
for (const QChar &ch : input) {
value = value * 10 + digitMap.value(ch);

Check warning on line 416 in src/dfm-search/dfm-search-lib/semantic/extractors/timeextractor.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Consider using std::accumulate algorithm instead of a raw loop.
}
return value;
}

return -1;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,15 +249,16 @@
},
{
"id": "time_exact_year",
"pattern": "(?<year>\\d{2,4})年",
"description": "Exact year (e.g. 2025年, 25年)",
"pattern": "(?<year>\\d{2,4}|[零〇一二三四五六七八九]{2,4})年",
"description": "Exact year (e.g. 2025年, 25年, 二五年, 二零二五年)",
"enabled": true,
"priority": 120,
"metadata": {
"type": "custom",
"format": "year",
"digit_map": {
"零": 0,
"〇": 0,
"一": 1,
"二": 2,
"三": 3,
Expand Down Expand Up @@ -299,15 +300,16 @@
},
{
"id": "time_exact_year_month",
"pattern": "(?<year>\\d{2,4})[年\\./\\-](?<month>(?:\\d{1,2}|[零一二两三四五六七八九十]{1,3}))(?:月份?)?",
"description": "Exact year-month (e.g. 2025年12月, 2025-12)",
"pattern": "(?<year>\\d{2,4}|[零〇一二三四五六七八九]{2,4})[年\\./\\-](?<month>(?:\\d{1,2}|[零一二两三四五六七八九十]{1,3}))(?:月份?)?",
"description": "Exact year-month (e.g. 2025年12月, 25年12月, 二五年十二月, 二零二五年12月)",
"enabled": true,
"priority": 140,
"metadata": {
"type": "custom",
"format": "year_month",
"digit_map": {
"零": 0,
"〇": 0,
"一": 1,
"二": 2,
"三": 3,
Expand Down Expand Up @@ -349,15 +351,16 @@
},
{
"id": "time_exact_full_date",
"pattern": "(?<year>\\d{2,4})[年\\./\\-](?<month>(?:\\d{1,2}|[零一二两三四五六七八九十]{1,3}))[月\\./\\-](?<day>(?:\\d{1,2}|[零一二两三四五六七八九十]{1,3}))[日号]?",
"description": "Exact full date (e.g. 2025年12月5日, 2025-12-05)",
"pattern": "(?<year>\\d{2,4}|[零〇一二三四五六七八九]{2,4})[年\\./\\-](?<month>(?:\\d{1,2}|[零一二两三四五六七八九十]{1,3}))[月\\./\\-](?<day>(?:\\d{1,2}|[零一二两三四五六七八九十]{1,3}))[日号]?",
"description": "Exact full date (e.g. 2025年12月5日, 2025-12-05, 二零二五年十二月五日)",
"enabled": true,
"priority": 160,
"metadata": {
"type": "custom",
"format": "full_date",
"digit_map": {
"零": 0,
"〇": 0,
"一": 1,
"二": 2,
"三": 3,
Expand Down
Loading