Files
ptimeparse/tests/__init__.py
2025-10-23 22:09:31 +08:00

357 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()}."
)