This commit is contained in:
2025-10-23 22:09:31 +08:00
parent d26fabe5d1
commit ec540eed55
3 changed files with 951 additions and 849 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,15 @@
[project]
name = "ptimeparse"
version = "0.1.2"
version = "0.2.0"
description = "一个用于解析中文的时间表达的库"
authors = [
{name = "passthem", email = "Passthem183@gmail.com"}
]
authors = [{ name = "passthem", email = "Passthem183@gmail.com" }]
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
]
dependencies = []
license = "MIT"
[tool.poetry]
packages = [{include = "ptimeparse" }]
packages = [{ include = "ptimeparse" }]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
@ -24,6 +21,4 @@ url = "https://gitea.service.jazzwhom.top/api/packages/Passthem/pypi"
priority = "supplemental"
[dependency-groups]
dev = [
"pytest (>=8.4.2,<9.0.0)"
]
dev = ["pytest (>=8.4.2,<9.0.0)"]

View File

@ -1,4 +1,4 @@
import datetime
from datetime import datetime as dt
import pytest
@ -6,192 +6,351 @@ from ptimeparse import Parser
from ptimeparse.err import MultipleSpecificationException, TokenUnhandledException
def test_chinese_number():
# --- 测试中文数字解析 ---
@pytest.mark.parametrize(
"input_str, expected_rest, expected_num",
[
("", "", 0),
("零喵", "", 0),
("一喵", "", 1),
("十喵", "", 10),
("三千万喵", "", 3000_0000),
("三千三百万喵", "", 3300_0000),
("三千零三万喵", "", 3003_0000),
("三千零三十万喵", "", 3030_0000),
("五千四百零三万喵", "", 5403_0000),
("五百万喵", "", 500_0000),
("五万五千喵", "", 5_5000),
("五万零五百喵", "", 5_0500),
("五亿喵", "", 5_0000_0000),
("五百亿喵", "", 500_0000_0000),
("五百亿零五十喵", "", 500_0000_0050),
("五百亿五十万喵", "", 500_0050_0000),
],
)
def test_digest_chinese_number(input_str, expected_rest, expected_num):
parser = Parser()
assert parser.digest_chinese_number("") == ('', 0)
assert parser.digest_chinese_number("零喵") == ('', 0)
assert parser.digest_chinese_number("一喵") == ('', 1)
assert parser.digest_chinese_number("十喵") == ('', 10)
assert parser.digest_chinese_number("三千万喵") == ('', 3000_0000)
assert parser.digest_chinese_number("三千三百万喵") == ('', 3300_0000)
assert parser.digest_chinese_number("三千零三万喵") == ('', 3003_0000)
assert parser.digest_chinese_number("三千零三十万喵") == ('', 3030_0000)
assert parser.digest_chinese_number("五千四百零三万喵") == ('', 5403_0000)
assert parser.digest_chinese_number("五百万喵") == ('', 500_0000)
assert parser.digest_chinese_number("五万五千喵") == ('', 5_5000)
assert parser.digest_chinese_number("五万零五百喵") == ('', 5_0500)
assert parser.digest_chinese_number("五亿喵") == ('', 5_0000_0000)
assert parser.digest_chinese_number("五百亿喵") == ('', 500_0000_0000)
assert parser.digest_chinese_number("五百亿零五十喵") == ('', 500_0000_0050)
assert parser.digest_chinese_number("五百亿五十万喵") == ('', 500_0050_0000)
rest, num = parser.digest_chinese_number(input_str)
# 使用 f-string 包含上下文信息
assert rest == expected_rest, f"Input: {input_str}, Expected Rest: {expected_rest}, Actual Rest: {rest}"
assert num == expected_num, f"Input: {input_str}, Expected Num: {expected_num}, Actual Num: {num}"
def test_hour_specification_pm():
parser = Parser(now=datetime.datetime(2025, 10, 9, 16, 34, 1, 114))
# --- 测试时间解析PM 上下文)---
@pytest.mark.parametrize(
"text, expected",
[
# 基础点表达(自动转为 PM if 1-12 且上下文为下午)
("五点", dt(2025, 10, 9, 17, 0)),
("5点", dt(2025, 10, 9, 17, 0)),
("5 点", dt(2025, 10, 9, 17, 0)),
("六点", dt(2025, 10, 9, 18, 0)),
("六点整", dt(2025, 10, 9, 18, 0)),
("六点钟", dt(2025, 10, 9, 18, 0)),
("四点", dt(2025, 10, 9, 16, 0)),
assert parser.parse("五点") == datetime.datetime(2025, 10, 9, 17, 0, 0, 0)
assert parser.parse("5点") == datetime.datetime(2025, 10, 9, 17, 0, 0, 0)
assert parser.parse("5 点") == datetime.datetime(2025, 10, 9, 17, 0, 0, 0)
# 显式 "时" 表示 24 小时制
("10 时", dt(2025, 10, 9, 10, 0)),
("10 时整", dt(2025, 10, 9, 10, 0)),
("13点", dt(2025, 10, 9, 13, 0)),
("15点", dt(2025, 10, 9, 15, 0)),
("13 时", dt(2025, 10, 9, 13, 0)),
("15 时", dt(2025, 10, 9, 15, 0)),
assert parser.parse("六点") == datetime.datetime(2025, 10, 9, 18, 0, 0, 0)
assert parser.parse("六点整") == datetime.datetime(2025, 10, 9, 18, 0, 0, 0)
assert parser.parse("六点钟") == datetime.datetime(2025, 10, 9, 18, 0, 0, 0)
# 显式上午/下午
("上午十点", dt(2025, 10, 9, 10, 0)),
("早晨十点", dt(2025, 10, 9, 10, 0)),
("早上十点", dt(2025, 10, 9, 10, 0)),
("早十", dt(2025, 10, 9, 10, 0)),
("早八", dt(2025, 10, 9, 8, 0)),
("晚六", dt(2025, 10, 9, 18, 0)),
("下午三点", dt(2025, 10, 9, 15, 0)),
("晚上八点", dt(2025, 10, 9, 20, 0)),
("中午十二点", dt(2025, 10, 9, 12, 0)),
("凌晨零点", dt(2025, 10, 9, 0, 0)),
assert parser.parse("10 时") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("10 时整") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("10点") == datetime.datetime(2025, 10, 9, 22, 0, 0, 0)
assert parser.parse("10点整") == datetime.datetime(2025, 10, 9, 22, 0, 0, 0)
# 特殊:晚上十二点 → 次日 00:00
("晚上十二点", dt(2025, 10, 10, 0, 0)),
assert parser.parse("13点") == datetime.datetime(2025, 10, 9, 13, 0, 0, 0)
assert parser.parse("15") == datetime.datetime(2025, 10, 9, 15, 0, 0, 0)
assert parser.parse("13 时") == datetime.datetime(2025, 10, 9, 13, 0, 0, 0)
assert parser.parse("15 时") == datetime.datetime(2025, 10, 9, 15, 0, 0, 0)
assert parser.parse("四点") == datetime.datetime(2025, 10, 9, 16, 0, 0, 0)
assert parser.parse("上午十点") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("早晨十点") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("早上十点") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("早十") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("早八") == datetime.datetime(2025, 10, 9, 8, 0, 0, 0)
assert parser.parse("晚六") == datetime.datetime(2025, 10, 9, 18, 0, 0, 0)
assert parser.parse("下午三点") == datetime.datetime(2025, 10, 9, 15, 0, 0, 0)
assert parser.parse("晚上十二点") == datetime.datetime(2025, 10, 10, 0, 0, 0, 0)
assert parser.parse("晚上八点") == datetime.datetime(2025, 10, 9, 20, 0, 0, 0)
assert parser.parse("凌晨零点") == datetime.datetime(2025, 10, 9, 0, 0, 0, 0)
assert parser.parse("中午十二点") == datetime.datetime(2025, 10, 9, 12, 0, 0, 0)
def test_hour_specification_am():
parser = Parser(now=datetime.datetime(2025, 10, 9, 2, 34, 1, 114))
assert parser.parse("五点") == datetime.datetime(2025, 10, 9, 5, 0, 0, 0)
assert parser.parse("5点") == datetime.datetime(2025, 10, 9, 5, 0, 0, 0)
assert parser.parse("5 点") == datetime.datetime(2025, 10, 9, 5, 0, 0, 0)
assert parser.parse("六点") == datetime.datetime(2025, 10, 9, 6, 0, 0, 0)
assert parser.parse("六点整") == datetime.datetime(2025, 10, 9, 6, 0, 0, 0)
assert parser.parse("六点钟") == datetime.datetime(2025, 10, 9, 6, 0, 0, 0)
assert parser.parse("10 时") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("10 时整") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("10点") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("10点整") == datetime.datetime(2025, 10, 9, 10, 0, 0, 0)
assert parser.parse("四点") == datetime.datetime(2025, 10, 9, 4, 0, 0, 0)
assert parser.parse("一点钟") == datetime.datetime(2025, 10, 9, 13, 0, 0, 0)
# 注意10点无修饰在 PM 上下文中 → 22:00
("10", dt(2025, 10, 9, 22, 0)),
("10点整", dt(2025, 10, 9, 22, 0)),
],
)
def test_parse_hour_pm_context(text, expected):
"""在下午16:34上下文中解析时间"""
NOW = dt(2025, 10, 9, 16, 34, 1, 114)
parser = Parser(now=NOW)
actual = parser.parse(text)
# 移除微秒,以便比较
expected_clean = expected.replace(microsecond=0)
actual_clean = actual.replace(microsecond=0)
# 使用 f-string 包含上下文信息输入文本、now 上下文、计算值、期望值
assert actual_clean == expected_clean, (
f"Failed on Text: '{text}'. "
f"Context (now): {NOW}. "
f"Expected: {expected_clean}. "
f"Actual: {actual_clean}."
)
def test_hour_with_minute():
parser = Parser(now=datetime.datetime(2025, 10, 9, 16, 34, 1, 114))
assert parser.parse("六点半") == datetime.datetime(2025, 10, 9, 18, 30, 0, 0)
assert parser.parse("六点半钟") == datetime.datetime(2025, 10, 9, 18, 30, 0, 0)
assert parser.parse("六点一刻") == datetime.datetime(2025, 10, 9, 18, 15, 0, 0)
assert parser.parse("六点过一刻") == datetime.datetime(2025, 10, 9, 18, 15, 0, 0)
# --- 测试时间解析AM 上下文)---
@pytest.mark.parametrize(
"text, expected",
[
("五点", dt(2025, 10, 9, 5, 0)),
("5点", dt(2025, 10, 9, 5, 0)),
("5 点", dt(2025, 10, 9, 5, 0)),
("六点", dt(2025, 10, 9, 6, 0)),
("六点整", dt(2025, 10, 9, 6, 0)),
("六点钟", dt(2025, 10, 9, 6, 0)),
("10 时", dt(2025, 10, 9, 10, 0)),
("10 时整", dt(2025, 10, 9, 10, 0)),
("10点", dt(2025, 10, 9, 10, 0)), # AM 上下文中 10点 → 10:00
("10点整", dt(2025, 10, 9, 10, 0)),
("四点", dt(2025, 10, 9, 4, 0)),
# 一点钟在 AM 上下文?但 13点是合理的可能表示下午1点
("一点钟", dt(2025, 10, 9, 13, 0)),
("晚上十二点", dt(2025, 10, 10, 0, 0)),
],
)
def test_parse_hour_am_context(text, expected):
"""在凌晨02:34上下文中解析时间"""
NOW = dt(2025, 10, 9, 2, 34, 1, 114)
parser = Parser(now=NOW)
actual = parser.parse(text)
expected_clean = expected.replace(microsecond=0)
actual_clean = actual.replace(microsecond=0)
assert actual_clean == expected_clean, (
f"Failed on Text: '{text}'. "
f"Context (now): {NOW}. "
f"Expected: {expected_clean}. "
f"Actual: {actual_clean}."
)
def test_error():
parser = Parser(now=datetime.datetime(2025, 10, 9, 16, 34, 1, 114))
with pytest.raises(TokenUnhandledException):
parser.parse("六点半")
with pytest.raises(MultipleSpecificationException):
parser.parse("六点一刻")
# --- 测试带分钟的时间 ---
@pytest.mark.parametrize(
"text, expected",
[
("六点半", dt(2025, 10, 9, 18, 30)),
("六点半钟", dt(2025, 10, 9, 18, 30)),
("六点一刻", dt(2025, 10, 9, 18, 15)),
("六点一刻", dt(2025, 10, 9, 18, 15)),
],
)
def test_parse_hour_with_minute(text, expected):
NOW = dt(2025, 10, 9, 16, 34, 1, 114)
parser = Parser(now=NOW)
actual = parser.parse(text)
expected_clean = expected.replace(microsecond=0)
actual_clean = actual.replace(microsecond=0)
assert actual_clean == expected_clean, (
f"Failed on Text: '{text}'. "
f"Context (now): {NOW}. "
f"Expected: {expected_clean}. "
f"Actual: {actual_clean}."
)
def test_absolute_date():
now = datetime.datetime(2025, 10, 9, 16, 34, 1, 114)
parser = Parser(now=now)
# 完整年月日
assert parser.parse("2025年10月9日") == datetime.datetime(2025, 10, 9, 0, 0, 0, 0)
assert parser.parse("2025-10-09") == datetime.datetime(2025, 10, 9, 0, 0, 0, 0)
assert parser.parse("2025/10/09") == datetime.datetime(2025, 10, 9, 0, 0, 0, 0)
# 仅月日(默认今年)
assert parser.parse("10月9日") == datetime.datetime(2025, 10, 9, 0, 0, 0, 0)
assert parser.parse("十月九日") == datetime.datetime(2025, 10, 9, 0, 0, 0, 0)
# 年月日 + 时间
assert parser.parse("2025年10月9日 15点") == datetime.datetime(2025, 10, 9, 15, 0, 0, 0)
assert parser.parse("10月9日 下午3点") == datetime.datetime(2025, 10, 9, 15, 0, 0, 0)
assert parser.parse("十月九日 晚上八点") == datetime.datetime(2025, 10, 9, 20, 0, 0, 0)
# ISO 格式(如果支持)
assert parser.parse("2025-10-09T15:30") == datetime.datetime(2025, 10, 9, 15, 30, 0, 0)
# --- 错误处理测试 ---
def test_parse_errors():
NOW = dt(2025, 10, 9, 16, 34, 1, 114)
parser = Parser(now=NOW)
def test_absolute_time():
now = datetime.datetime(2025, 10, 9, 16, 34, 1, 114)
parser = Parser(now=now)
assert parser.parse("5:30") == datetime.datetime(2025, 10, 9, 17, 30, 0, 0)
assert parser.parse("5:11") == datetime.datetime(2025, 10, 9, 17, 11, 0, 0)
assert parser.parse("5点30分") == datetime.datetime(2025, 10, 9, 17, 30, 0, 0)
assert parser.parse("17:20") == datetime.datetime(2025, 10, 9, 17, 20, 0, 0)
def test_relative_date():
now = datetime.datetime(2025, 10, 9, 10, 0, 0)
parser = Parser(now=now)
assert parser.parse("明天") == datetime.datetime(2025, 10, 10, 0, 0, 0, 0)
assert parser.parse("后天") == datetime.datetime(2025, 10, 11, 0, 0, 0, 0)
assert parser.parse("昨天") == datetime.datetime(2025, 10, 8, 0, 0, 0, 0)
assert parser.parse("大前天") == datetime.datetime(2025, 10, 6, 0, 0, 0, 0)
assert parser.parse("大后天") == datetime.datetime(2025, 10, 12, 0, 0, 0, 0)
assert parser.parse("三天后") == datetime.datetime(2025, 10, 12, 0, 0, 0, 0)
assert parser.parse("五天前") == datetime.datetime(2025, 10, 4, 0, 0, 0, 0)
assert parser.parse("下周一") == datetime.datetime(2025, 10, 13, 0, 0, 0, 0)
assert parser.parse("上周五") == datetime.datetime(2025, 10, 3, 0, 0, 0, 0)
assert parser.parse("本周五") == datetime.datetime(2025, 10, 10, 0, 0, 0, 0)
end_of_month = datetime.datetime(2025, 10, 31, 10, 0, 0)
parser2 = Parser(now=end_of_month)
assert parser2.parse("两天后") == datetime.datetime(2025, 11, 2, 0, 0, 0, 0)
def test_relative_time():
now = datetime.datetime(2025, 10, 9, 16, 30, 0, 0)
parser = Parser(now=now)
assert parser.parse("五分钟后") == datetime.datetime(2025, 10, 9, 16, 35, 0, 0)
assert parser.parse("十分钟前") == datetime.datetime(2025, 10, 9, 16, 20, 0, 0)
assert parser.parse("半小时后") == datetime.datetime(2025, 10, 9, 17, 0, 0, 0)
assert parser.parse("一个半小时后") == datetime.datetime(2025, 10, 9, 18, 0, 0, 0)
assert parser.parse("两小时后") == datetime.datetime(2025, 10, 9, 18, 30, 0, 0)
assert parser.parse("一小时前") == datetime.datetime(2025, 10, 9, 15, 30, 0, 0)
late_night = datetime.datetime(2025, 10, 9, 23, 50, 0, 0)
parser3 = Parser(now=late_night)
assert parser3.parse("二十分钟后") == datetime.datetime(2025, 10, 10, 0, 10, 0, 0)
assert parser.parse("5分钟后") == datetime.datetime(2025, 10, 9, 16, 35, 0, 0)
assert parser.parse("三十分钟前") == datetime.datetime(2025, 10, 9, 16, 0, 0, 0)
def test_robustness_edge_cases():
parser = Parser(now=datetime.datetime(2025, 2, 28, 10, 0, 0))
assert parser.parse("明天") == datetime.datetime(2025, 3, 1, 0, 0, 0, 0)
parser_leap = Parser(now=datetime.datetime(2024, 2, 28, 10, 0, 0))
assert parser_leap.parse("两天后") == datetime.datetime(2024, 3, 1, 0, 0, 0, 0)
with pytest.raises(TokenUnhandledException):
# 完全无效输入
with pytest.raises(TokenUnhandledException, match="随便乱写"):
parser.parse("随便乱写")
parser.parse(" 明天 ")
def test_mixed_expressions():
now = datetime.datetime(2025, 10, 9, 14, 0, 0)
# 但允许前后空格
result = parser.parse(" 明天 ")
# 使用 f-string 包含上下文信息
expected_date = dt(2025, 10, 10).date()
assert result.date() == expected_date, (
f"Failed on Text: ' 明天 '. "
f"Context (now): {NOW}. "
f"Expected Date: {expected_date}. "
f"Actual Date: {result.date()}."
)
# --- 绝对日期测试 ---
@pytest.mark.parametrize(
"text, expected",
[
("2025年10月9日", dt(2025, 10, 9, 0, 0)),
("2025-10-09", dt(2025, 10, 9, 0, 0)),
("2025/10/09", dt(2025, 10, 9, 0, 0)),
("10月9日", dt(2025, 10, 9, 0, 0)),
("十月九日", dt(2025, 10, 9, 0, 0)),
("2025年10月9日 15点", dt(2025, 10, 9, 15, 0)),
("10月9日 下午3点", dt(2025, 10, 9, 15, 0)),
("十月九日 晚上八点", dt(2025, 10, 9, 20, 0)),
("2025-10-09T15:30", dt(2025, 10, 9, 15, 30)),
],
)
def test_parse_absolute_date(text, expected):
NOW = dt(2025, 10, 9, 16, 34, 1, 114)
parser = Parser(now=NOW)
actual = parser.parse(text)
expected_clean = expected.replace(microsecond=0)
actual_clean = actual.replace(microsecond=0)
assert actual_clean == expected_clean, (
f"Failed on Text: '{text}'. "
f"Context (now): {NOW}. "
f"Expected: {expected_clean}. "
f"Actual: {actual_clean}."
)
# --- 绝对时间(无日期)测试 ---
@pytest.mark.parametrize(
"text, expected",
[
("5:30", dt(2025, 10, 9, 17, 30)),
("5:11", dt(2025, 10, 9, 17, 11)),
("5点30分", dt(2025, 10, 9, 17, 30)),
("17:20", dt(2025, 10, 9, 17, 20)),
("六点零五", dt(2025, 10, 9, 18, 5, 0, 0)),
],
)
def test_parse_absolute_time(text, expected):
NOW = dt(2025, 10, 9, 16, 34, 1, 114)
parser = Parser(now=NOW)
actual = parser.parse(text)
expected_clean = expected.replace(microsecond=0)
actual_clean = actual.replace(microsecond=0)
assert actual_clean == expected_clean, (
f"Failed on Text: '{text}'. "
f"Context (now): {NOW}. "
f"Expected: {expected_clean}. "
f"Actual: {actual_clean}."
)
# --- 相对日期测试 ---
@pytest.mark.parametrize(
"now, text, expected",
[
(dt(2025, 10, 9, 10, 0), "明天", dt(2025, 10, 10)),
(dt(2025, 10, 9, 10, 0), "后天", dt(2025, 10, 11)),
(dt(2025, 10, 9, 10, 0), "昨天", dt(2025, 10, 8)),
(dt(2025, 10, 9, 10, 0), "大前天", dt(2025, 10, 6)),
(dt(2025, 10, 9, 10, 0), "大后天", dt(2025, 10, 12)),
(dt(2025, 10, 9, 10, 0), "三天后", dt(2025, 10, 12)),
(dt(2025, 10, 9, 10, 0), "五天前", dt(2025, 10, 4)),
(dt(2025, 10, 9, 10, 0), "下周一", dt(2025, 10, 13)), # 10-9 是周四
(dt(2025, 10, 9, 10, 0), "上周五", dt(2025, 10, 3)),
(dt(2025, 10, 9, 10, 0), "本周五", dt(2025, 10, 10)),
(dt(2025, 10, 31, 10, 0), "两天后", dt(2025, 11, 2)),
],
)
def test_parse_relative_date(now, text, expected):
parser = Parser(now=now)
# 如“明天下午三点”
assert parser.parse("明天下午三点") == datetime.datetime(2025, 10, 10, 15, 0, 0, 0)
assert parser.parse("后天早上八点") == datetime.datetime(2025, 10, 11, 8, 0, 0, 0)
assert parser.parse("大后天晚上十点") == datetime.datetime(2025, 10, 12, 22, 0, 0, 0)
# “下周三上午”
# 2025-10-09 是周四,下周三是 2025-10-15
assert parser.parse("下周三") == datetime.datetime(2025, 10, 15, 0, 0, 0, 0)
assert parser.parse("下周三早八") == datetime.datetime(2025, 10, 15, 8, 0, 0, 0) # 默认0点或上午9点需根据实现
# 若实现中“上午”不指定小时则设为9点可调整这里假设设为0点以简化
actual = parser.parse(text)
expected_clean = expected.replace(microsecond=0)
actual_clean = actual.replace(microsecond=0)
assert actual_clean == expected_clean, (
f"Failed on Text: '{text}'. "
f"Context (now): {now}. "
f"Expected: {expected_clean}. "
f"Actual: {actual_clean}."
)
# --- 相对时间(分钟/小时)测试 ---
@pytest.mark.parametrize(
"now, text, expected",
[
(dt(2025, 10, 9, 16, 30), "五分钟后", dt(2025, 10, 9, 16, 35)),
(dt(2025, 10, 9, 16, 30), "十分钟前", dt(2025, 10, 9, 16, 20)),
(dt(2025, 10, 9, 16, 30), "半小时后", dt(2025, 10, 9, 17, 0)),
(dt(2025, 10, 9, 16, 30), "一个半小时后", dt(2025, 10, 9, 18, 0)),
(dt(2025, 10, 9, 16, 30), "两个半小时后", dt(2025, 10, 9, 19, 0)),
(dt(2025, 10, 9, 16, 30), "两小时后", dt(2025, 10, 9, 18, 30)),
(dt(2025, 10, 9, 16, 30), "一小时前", dt(2025, 10, 9, 15, 30)),
(dt(2025, 10, 9, 23, 50), "二十分钟后", dt(2025, 10, 10, 0, 10)),
(dt(2025, 10, 9, 16, 30), "5分钟后", dt(2025, 10, 9, 16, 35)),
(dt(2025, 10, 9, 16, 30), "三十分钟前", dt(2025, 10, 9, 16, 0)),
(dt(2025, 10, 9, 16, 30, 0, 0), "两秒后", dt(2025, 10, 9, 16, 30, 2, 0)),
# 同义词支持
(dt(2025, 10, 19, 20, 16), "一小时后", dt(2025, 10, 19, 21, 16)),
(dt(2025, 10, 19, 20, 16), "一小时以后", dt(2025, 10, 19, 21, 16)),
(dt(2025, 10, 19, 20, 16), "一小时之后", dt(2025, 10, 19, 21, 16)),
(dt(2025, 10, 19, 20, 16), "一个小时以后", dt(2025, 10, 19, 21, 16)),
],
)
def test_parse_relative_time(now, text, expected):
parser = Parser(now=now)
actual = parser.parse(text)
expected_clean = expected.replace(microsecond=0)
actual_clean = actual.replace(microsecond=0)
assert actual_clean == expected_clean, (
f"Failed on Text: '{text}'. "
f"Context (now): {now}. "
f"Expected: {expected_clean}. "
f"Actual: {actual_clean}."
)
# --- 混合表达式(日期 + 时间)---
@pytest.mark.parametrize(
"now, text, expected",
[
(dt(2025, 10, 9, 14, 0), "明天下午三点", dt(2025, 10, 10, 15, 0)),
(dt(2025, 10, 9, 14, 0), "后天早上八点", dt(2025, 10, 11, 8, 0)),
(dt(2025, 10, 9, 14, 0), "大后天晚上十点", dt(2025, 10, 12, 22, 0)),
(dt(2025, 10, 9, 14, 0), "下周三", dt(2025, 10, 15, 0, 0)), # 10-9 周四 → 下周三 10-15
(dt(2025, 10, 9, 14, 0), "下周三早八", dt(2025, 10, 15, 8, 0)),
(dt(2025, 10, 19, 20, 16), "八点二十", dt(2025, 10, 19, 20, 20)),
(dt(2025, 10, 19, 20, 16), "明天八点二十", dt(2025, 10, 20, 8, 20)),
(dt(2025, 10, 19, 10, 10), "今晚八点", dt(2025, 10, 19, 20, 0)),
(dt(2025, 10, 19, 10, 10), "今天早上六点", dt(2025, 10, 19, 6, 0)),
(dt(2025, 10, 19, 10, 10), "今早七点五十分", dt(2025, 10, 19, 7, 50)),
],
)
def test_parse_mixed_expressions(now, text, expected):
parser = Parser(now=now)
actual = parser.parse(text)
expected_clean = expected.replace(microsecond=0)
actual_clean = actual.replace(microsecond=0)
assert actual_clean == expected_clean, (
f"Failed on Text: '{text}'. "
f"Context (now): {now}. "
f"Expected: {expected_clean}. "
f"Actual: {actual_clean}."
)
# --- 边界情况与鲁棒性 ---
def test_robustness_edge_cases():
# 闰年 & 月末
parser_feb = Parser(now=dt(2025, 2, 28, 10, 0))
expected_march = dt(2025, 3, 1, 0, 0)
actual_march = parser_feb.parse("明天")
assert actual_march == expected_march, (
f"Failed on '明天' (now=2025-02-28). "
f"Expected: {expected_march}. "
f"Actual: {actual_march}."
)
parser_leap = Parser(now=dt(2024, 2, 28, 10, 0))
expected_leap = dt(2024, 3, 1, 0, 0)
actual_leap = parser_leap.parse("两天后")
assert actual_leap == expected_leap, (
f"Failed on '两天后' (now=2024-02-28 - Leap Year). "
f"Expected: {expected_leap}. "
f"Actual: {actual_leap}."
)
# 空格容忍
NOW = dt(2025, 10, 9, 10, 0)
parser = Parser(now=NOW)
result = parser.parse(" 明天 ")
expected_date = dt(2025, 10, 10).date()
assert result.date() == expected_date, (
f"Failed on Text: ' 明天 ' (Whitespace). "
f"Context (now): {NOW}. "
f"Expected Date: {expected_date}. "
f"Actual Date: {result.date()}."
)