357 lines
13 KiB
Python
357 lines
13 KiB
Python
from datetime import datetime as dt
|
||
|
||
import pytest
|
||
|
||
from ptimeparse import Parser
|
||
from ptimeparse.err import MultipleSpecificationException, TokenUnhandledException
|
||
|
||
|
||
# --- 测试中文数字解析 ---
|
||
@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()
|
||
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}"
|
||
|
||
|
||
# --- 测试时间解析(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)),
|
||
|
||
# 显式 "时" 表示 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)),
|
||
|
||
# 显式上午/下午
|
||
("上午十点", 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)),
|
||
|
||
# 特殊:晚上十二点 → 次日 00:00
|
||
("晚上十二点", dt(2025, 10, 10, 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}."
|
||
)
|
||
|
||
|
||
# --- 测试时间解析(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}."
|
||
)
|
||
|
||
|
||
# --- 测试带分钟的时间 ---
|
||
@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_parse_errors():
|
||
NOW = dt(2025, 10, 9, 16, 34, 1, 114)
|
||
parser = Parser(now=NOW)
|
||
|
||
# 完全无效输入
|
||
with pytest.raises(TokenUnhandledException, match="随便乱写"):
|
||
parser.parse("随便乱写")
|
||
|
||
# 但允许前后空格
|
||
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)
|
||
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()}."
|
||
)
|