485 lines
14 KiB
C++
485 lines
14 KiB
C++
#include "ReminderParser.h"
|
||
|
||
#include <QDate>
|
||
#include <QRegularExpression>
|
||
#include <QStringList>
|
||
#include <QTime>
|
||
|
||
namespace
|
||
{
|
||
bool containsAny(const QString &text, const QStringList &keywords)
|
||
{
|
||
for (const QString &keyword : keywords)
|
||
{
|
||
if (text.contains(keyword))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
int chineseDigitValue(QChar value)
|
||
{
|
||
if (value == QLatin1Char('0') || value == QChar(0x3007) || value == QStringLiteral("零").at(0))
|
||
{
|
||
return 0;
|
||
}
|
||
if (value == QStringLiteral("一").at(0))
|
||
{
|
||
return 1;
|
||
}
|
||
if (value == QStringLiteral("二").at(0) || value == QStringLiteral("两").at(0))
|
||
{
|
||
return 2;
|
||
}
|
||
if (value == QStringLiteral("三").at(0))
|
||
{
|
||
return 3;
|
||
}
|
||
if (value == QStringLiteral("四").at(0))
|
||
{
|
||
return 4;
|
||
}
|
||
if (value == QStringLiteral("五").at(0))
|
||
{
|
||
return 5;
|
||
}
|
||
if (value == QStringLiteral("六").at(0))
|
||
{
|
||
return 6;
|
||
}
|
||
if (value == QStringLiteral("七").at(0))
|
||
{
|
||
return 7;
|
||
}
|
||
if (value == QStringLiteral("八").at(0))
|
||
{
|
||
return 8;
|
||
}
|
||
if (value == QStringLiteral("九").at(0))
|
||
{
|
||
return 9;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
int parseSmallInteger(QString value)
|
||
{
|
||
value = value.trimmed();
|
||
value.remove(QStringLiteral("个"));
|
||
if (value.isEmpty())
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
bool ok = false;
|
||
const int numericValue = value.toInt(&ok);
|
||
if (ok)
|
||
{
|
||
return numericValue;
|
||
}
|
||
|
||
const int tenIndex = value.indexOf(QStringLiteral("十"));
|
||
if (tenIndex >= 0)
|
||
{
|
||
const QString left = value.left(tenIndex);
|
||
const QString right = value.mid(tenIndex + 1);
|
||
const int tens = left.isEmpty() ? 1 : parseSmallInteger(left);
|
||
const int ones = right.isEmpty() ? 0 : parseSmallInteger(right);
|
||
if (tens < 0 || ones < 0)
|
||
{
|
||
return -1;
|
||
}
|
||
return tens * 10 + ones;
|
||
}
|
||
|
||
if (value.size() == 1)
|
||
{
|
||
return chineseDigitValue(value.at(0));
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
QString cleanedText(QString text)
|
||
{
|
||
return text
|
||
.replace(QChar(0xff0c), QStringLiteral(" "))
|
||
.replace(QChar(0x3002), QStringLiteral(" "))
|
||
.replace(QChar(0xff1a), QStringLiteral(":"))
|
||
.replace(QChar(0xff1b), QStringLiteral(" "))
|
||
.trimmed();
|
||
}
|
||
|
||
int adjustedHour(const QString &period, int hour)
|
||
{
|
||
if (hour < 0)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
if (period == QStringLiteral("下午") || period == QStringLiteral("晚上"))
|
||
{
|
||
return hour < 12 ? hour + 12 : hour;
|
||
}
|
||
|
||
if (period == QStringLiteral("中午"))
|
||
{
|
||
return hour < 11 ? hour + 12 : hour;
|
||
}
|
||
|
||
if (period == QStringLiteral("凌晨") && hour == 12)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
return hour;
|
||
}
|
||
|
||
int weekdayFromText(const QString &text)
|
||
{
|
||
const QString normalized = text.trimmed();
|
||
if (normalized == QStringLiteral("一") || normalized == QStringLiteral("1"))
|
||
{
|
||
return 1;
|
||
}
|
||
if (normalized == QStringLiteral("二") || normalized == QStringLiteral("2"))
|
||
{
|
||
return 2;
|
||
}
|
||
if (normalized == QStringLiteral("三") || normalized == QStringLiteral("3"))
|
||
{
|
||
return 3;
|
||
}
|
||
if (normalized == QStringLiteral("四") || normalized == QStringLiteral("4"))
|
||
{
|
||
return 4;
|
||
}
|
||
if (normalized == QStringLiteral("五") || normalized == QStringLiteral("5"))
|
||
{
|
||
return 5;
|
||
}
|
||
if (normalized == QStringLiteral("六") || normalized == QStringLiteral("6"))
|
||
{
|
||
return 6;
|
||
}
|
||
if (normalized == QStringLiteral("日")
|
||
|| normalized == QStringLiteral("天")
|
||
|| normalized == QStringLiteral("7"))
|
||
{
|
||
return 7;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
struct ReminderDateResolution
|
||
{
|
||
QDate date;
|
||
bool explicitDate = false;
|
||
};
|
||
|
||
ReminderDateResolution resolveReminderDate(const QString &text, const QDate ¤tDate)
|
||
{
|
||
QRegularExpressionMatch match;
|
||
|
||
const QRegularExpression nextWeekExpression(QStringLiteral("下周\\s*([一二三四五六日天1-7])"));
|
||
match = nextWeekExpression.match(text);
|
||
if (match.hasMatch())
|
||
{
|
||
const int targetWeekday = weekdayFromText(match.captured(1));
|
||
if (targetWeekday > 0)
|
||
{
|
||
const int daysToNextMonday = 8 - currentDate.dayOfWeek();
|
||
return {currentDate.addDays(daysToNextMonday + targetWeekday - 1), true};
|
||
}
|
||
}
|
||
|
||
const QRegularExpression monthDayExpression(QStringLiteral("(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*(?:日|号)?"));
|
||
match = monthDayExpression.match(text);
|
||
if (match.hasMatch())
|
||
{
|
||
const int month = match.captured(1).toInt();
|
||
const int day = match.captured(2).toInt();
|
||
QDate date(currentDate.year(), month, day);
|
||
if (date.isValid() && date < currentDate)
|
||
{
|
||
date = date.addYears(1);
|
||
}
|
||
return {date, true};
|
||
}
|
||
|
||
const QRegularExpression slashDateExpression(QStringLiteral("(\\d{1,2})\\s*/\\s*(\\d{1,2})"));
|
||
match = slashDateExpression.match(text);
|
||
if (match.hasMatch())
|
||
{
|
||
const int month = match.captured(1).toInt();
|
||
const int day = match.captured(2).toInt();
|
||
QDate date(currentDate.year(), month, day);
|
||
if (date.isValid() && date < currentDate)
|
||
{
|
||
date = date.addYears(1);
|
||
}
|
||
return {date, true};
|
||
}
|
||
|
||
if (text.contains(QStringLiteral("后天")))
|
||
{
|
||
return {currentDate.addDays(2), true};
|
||
}
|
||
|
||
if (text.contains(QStringLiteral("明天")))
|
||
{
|
||
return {currentDate.addDays(1), true};
|
||
}
|
||
|
||
if (text.contains(QStringLiteral("今天")))
|
||
{
|
||
return {currentDate, true};
|
||
}
|
||
|
||
return {currentDate, false};
|
||
}
|
||
|
||
QString removeFirst(const QString &text, const QString &part)
|
||
{
|
||
QString result = text;
|
||
const int index = result.indexOf(part);
|
||
if (index >= 0)
|
||
{
|
||
result.remove(index, part.size());
|
||
}
|
||
return result;
|
||
}
|
||
}
|
||
|
||
ReminderCommand ReminderParser::parse(const QString &text, const QDateTime &now) const
|
||
{
|
||
const QString normalized = cleanedText(text);
|
||
if (normalized.isEmpty())
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, {}, {}, {}, QStringLiteral("提醒内容为空。")};
|
||
}
|
||
|
||
if (containsAny(normalized, {QStringLiteral("提醒列表"), QStringLiteral("查看提醒"), QStringLiteral("我的提醒"), QStringLiteral("有哪些提醒")}))
|
||
{
|
||
return {ReminderCommandType::List, {}, normalized};
|
||
}
|
||
|
||
if (normalized.contains(QStringLiteral("取消提醒")) || normalized.startsWith(QStringLiteral("取消")))
|
||
{
|
||
QString query = normalized;
|
||
query.remove(QStringLiteral("取消提醒"));
|
||
if (query == normalized)
|
||
{
|
||
query.remove(QStringLiteral("取消"));
|
||
query.remove(QStringLiteral("提醒"));
|
||
}
|
||
query = query.trimmed();
|
||
if (query.isEmpty())
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, normalized, {}, {}, QStringLiteral("请说明要取消哪条提醒。")};
|
||
}
|
||
return {ReminderCommandType::Cancel, {}, normalized, {}, query};
|
||
}
|
||
|
||
return parseCreateCommand(normalized, now);
|
||
}
|
||
|
||
ReminderCommand ReminderParser::parseCreateCommand(const QString &text, const QDateTime &now) const
|
||
{
|
||
const QDate currentDate = now.date();
|
||
|
||
if (containsAny(text, {
|
||
QStringLiteral("每天"),
|
||
QStringLiteral("每日"),
|
||
QStringLiteral("每周"),
|
||
QStringLiteral("每星期"),
|
||
QStringLiteral("每月"),
|
||
QStringLiteral("每年"),
|
||
QStringLiteral("工作日"),
|
||
QStringLiteral("重复"),
|
||
}))
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("重复提醒尚未支持,目前只能创建一次性提醒。")};
|
||
}
|
||
|
||
const QRegularExpression relativeMinutesExpression(QStringLiteral("([0-9]+|[一二两三四五六七八九十]+)\\s*分钟后"));
|
||
QRegularExpressionMatch match = relativeMinutesExpression.match(text);
|
||
if (match.hasMatch())
|
||
{
|
||
const int minutes = parseSmallInteger(match.captured(1));
|
||
if (minutes <= 0)
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("提醒时间必须晚于当前时间。")};
|
||
}
|
||
|
||
const QString timeExpression = match.captured(0);
|
||
return {
|
||
ReminderCommandType::Create,
|
||
extractTitle(text, timeExpression),
|
||
text,
|
||
now.addSecs(minutes * 60),
|
||
{},
|
||
{},
|
||
};
|
||
}
|
||
|
||
const QRegularExpression relativeOneAndHalfHourExpression(QStringLiteral("([0-9]+|[一二两三四五六七八九十]+)\\s*(?:个)?半小时后"));
|
||
match = relativeOneAndHalfHourExpression.match(text);
|
||
if (match.hasMatch())
|
||
{
|
||
const int hours = parseSmallInteger(match.captured(1));
|
||
if (hours < 0)
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到有效提醒时间。")};
|
||
}
|
||
|
||
const QString timeExpression = match.captured(0);
|
||
return {
|
||
ReminderCommandType::Create,
|
||
extractTitle(text, timeExpression),
|
||
text,
|
||
now.addSecs(hours * 60 * 60 + 30 * 60),
|
||
{},
|
||
{},
|
||
};
|
||
}
|
||
|
||
if (text.contains(QStringLiteral("半小时后")))
|
||
{
|
||
const QString timeExpression = QStringLiteral("半小时后");
|
||
return {
|
||
ReminderCommandType::Create,
|
||
extractTitle(text, timeExpression),
|
||
text,
|
||
now.addSecs(30 * 60),
|
||
{},
|
||
{},
|
||
};
|
||
}
|
||
|
||
const QRegularExpression relativeHoursExpression(QStringLiteral("([0-9]+|[一二两三四五六七八九十]+)\\s*(?:个)?小时后"));
|
||
match = relativeHoursExpression.match(text);
|
||
if (match.hasMatch())
|
||
{
|
||
const int hours = parseSmallInteger(match.captured(1));
|
||
if (hours <= 0)
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("提醒时间必须晚于当前时间。")};
|
||
}
|
||
|
||
const QString timeExpression = match.captured(0);
|
||
return {
|
||
ReminderCommandType::Create,
|
||
extractTitle(text, timeExpression),
|
||
text,
|
||
now.addSecs(hours * 60 * 60),
|
||
{},
|
||
{},
|
||
};
|
||
}
|
||
|
||
const ReminderDateResolution dateResolution = resolveReminderDate(text, currentDate);
|
||
if (!dateResolution.date.isValid())
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到有效提醒日期。")};
|
||
}
|
||
|
||
const QRegularExpression clockExpression(QStringLiteral("(上午|早上|下午|晚上|中午|凌晨)?\\s*([0-9]{1,2}|[一二两三四五六七八九十]+)\\s*点\\s*(?:(\\d{1,2})\\s*分?)?"));
|
||
match = clockExpression.match(text);
|
||
if (match.hasMatch())
|
||
{
|
||
const QString period = match.captured(1);
|
||
int hour = adjustedHour(period, parseSmallInteger(match.captured(2)));
|
||
const int minute = match.captured(3).isEmpty() ? 0 : match.captured(3).toInt();
|
||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到有效提醒时间。")};
|
||
}
|
||
|
||
QDateTime remindAt(dateResolution.date, QTime(hour, minute));
|
||
if (remindAt <= now)
|
||
{
|
||
if (dateResolution.explicitDate)
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("提醒时间必须晚于当前时间。")};
|
||
}
|
||
remindAt = remindAt.addDays(1);
|
||
}
|
||
|
||
return {
|
||
ReminderCommandType::Create,
|
||
extractTitle(text, match.captured(0)),
|
||
text,
|
||
remindAt,
|
||
{},
|
||
{},
|
||
};
|
||
}
|
||
|
||
const QRegularExpression colonClockExpression(QStringLiteral("(明天)?\\s*(?:在)?\\s*([01]?\\d|2[0-3])\\s*[::]\\s*([0-5]\\d)"));
|
||
match = colonClockExpression.match(text);
|
||
if (match.hasMatch())
|
||
{
|
||
QDateTime remindAt(dateResolution.date, QTime(match.captured(2).toInt(), match.captured(3).toInt()));
|
||
if (remindAt <= now)
|
||
{
|
||
if (dateResolution.explicitDate)
|
||
{
|
||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("提醒时间必须晚于当前时间。")};
|
||
}
|
||
remindAt = remindAt.addDays(1);
|
||
}
|
||
|
||
return {
|
||
ReminderCommandType::Create,
|
||
extractTitle(text, match.captured(0)),
|
||
text,
|
||
remindAt,
|
||
{},
|
||
{},
|
||
};
|
||
}
|
||
|
||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到提醒时间。支持如“10分钟后提醒我喝水”“明天9点提醒我开会”。")};
|
||
}
|
||
|
||
QString ReminderParser::extractTitle(QString text, const QString &timeExpression) const
|
||
{
|
||
text = removeFirst(text, timeExpression);
|
||
const QStringList tokensToRemove = {
|
||
QStringLiteral("提醒我"),
|
||
QStringLiteral("提醒"),
|
||
QStringLiteral("叫我"),
|
||
QStringLiteral("到点"),
|
||
QStringLiteral("的时候"),
|
||
QStringLiteral("请"),
|
||
QStringLiteral("帮我"),
|
||
QStringLiteral("今天"),
|
||
QStringLiteral("明天"),
|
||
QStringLiteral("后天"),
|
||
};
|
||
|
||
for (const QString &token : tokensToRemove)
|
||
{
|
||
text.remove(token);
|
||
}
|
||
|
||
text.remove(QRegularExpression(QStringLiteral("\\d{1,2}\\s*月\\s*\\d{1,2}\\s*(?:日|号)?")));
|
||
text.remove(QRegularExpression(QStringLiteral("\\d{1,2}\\s*/\\s*\\d{1,2}")));
|
||
text.remove(QRegularExpression(QStringLiteral("下周\\s*[一二三四五六日天1-7]")));
|
||
|
||
text = text.trimmed();
|
||
while (text.startsWith(QChar(0x3000)) || text.startsWith(QLatin1Char(' ')) || text.startsWith(QLatin1Char(',')) || text.startsWith(QChar(0xff0c)))
|
||
{
|
||
text.remove(0, 1);
|
||
text = text.trimmed();
|
||
}
|
||
|
||
return text.isEmpty() ? QStringLiteral("提醒") : text;
|
||
}
|