129 lines
3.7 KiB
Python
129 lines
3.7 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import re
|
||
|
|
from datetime import UTC, date, datetime, timedelta
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
|
||
|
|
def expand_application_time_with_days(
|
||
|
|
time_text: str,
|
||
|
|
days_text: str,
|
||
|
|
*,
|
||
|
|
context_json: dict[str, Any] | None = None,
|
||
|
|
) -> str:
|
||
|
|
normalized_time = str(time_text or "").strip()
|
||
|
|
days = resolve_application_days_count(days_text)
|
||
|
|
if not days:
|
||
|
|
return normalized_time
|
||
|
|
|
||
|
|
if normalized_time and re.search(r"\s*(?:至|到|~|-{2,}|—)\s*", normalized_time):
|
||
|
|
return normalized_time
|
||
|
|
|
||
|
|
parsed_start = _resolve_start_date(normalized_time, context_json or {})
|
||
|
|
if parsed_start is None:
|
||
|
|
return normalized_time
|
||
|
|
|
||
|
|
end_date = parsed_start + timedelta(days=max(days - 1, 0))
|
||
|
|
start_text = f"{parsed_start:%Y-%m-%d}"
|
||
|
|
end_text = f"{end_date:%Y-%m-%d}"
|
||
|
|
return start_text if start_text == end_text else f"{start_text} 至 {end_text}"
|
||
|
|
|
||
|
|
|
||
|
|
def resolve_application_days_count(days_text: str) -> int:
|
||
|
|
text = str(days_text or "").strip()
|
||
|
|
if not text:
|
||
|
|
return 0
|
||
|
|
digit_match = re.search(r"\d+", text)
|
||
|
|
if digit_match:
|
||
|
|
return max(0, int(digit_match.group(0)))
|
||
|
|
|
||
|
|
chinese_match = re.search(r"[一二两三四五六七八九十]{1,3}", text)
|
||
|
|
if not chinese_match:
|
||
|
|
return 0
|
||
|
|
return _parse_chinese_number(chinese_match.group(0))
|
||
|
|
|
||
|
|
|
||
|
|
def _resolve_start_date(time_text: str, context_json: dict[str, Any]) -> date | None:
|
||
|
|
if time_text:
|
||
|
|
match = re.search(
|
||
|
|
r"(?P<date>20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?)",
|
||
|
|
time_text,
|
||
|
|
)
|
||
|
|
if match:
|
||
|
|
return _parse_application_date(match.group("date"))
|
||
|
|
return None
|
||
|
|
return _resolve_client_today(context_json)
|
||
|
|
|
||
|
|
|
||
|
|
def _resolve_client_today(context_json: dict[str, Any]) -> date:
|
||
|
|
raw_now = str(context_json.get("client_now_iso") or "").strip()
|
||
|
|
parsed_now = _parse_client_now(raw_now)
|
||
|
|
if parsed_now is None:
|
||
|
|
return datetime.now(UTC).date()
|
||
|
|
|
||
|
|
offset_minutes = _parse_timezone_offset_minutes(
|
||
|
|
context_json.get("client_timezone_offset_minutes"),
|
||
|
|
)
|
||
|
|
if offset_minutes is not None:
|
||
|
|
parsed_now = parsed_now - timedelta(minutes=offset_minutes)
|
||
|
|
return parsed_now.date()
|
||
|
|
|
||
|
|
|
||
|
|
def _parse_client_now(value: str) -> datetime | None:
|
||
|
|
if not value:
|
||
|
|
return None
|
||
|
|
normalized = value.replace("Z", "+00:00")
|
||
|
|
try:
|
||
|
|
parsed = datetime.fromisoformat(normalized)
|
||
|
|
except ValueError:
|
||
|
|
return None
|
||
|
|
if parsed.tzinfo is None:
|
||
|
|
parsed = parsed.replace(tzinfo=UTC)
|
||
|
|
return parsed.astimezone(UTC)
|
||
|
|
|
||
|
|
|
||
|
|
def _parse_timezone_offset_minutes(value: Any) -> int | None:
|
||
|
|
try:
|
||
|
|
return int(value)
|
||
|
|
except (TypeError, ValueError):
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def _parse_chinese_number(value: str) -> int:
|
||
|
|
digits = {
|
||
|
|
"一": 1,
|
||
|
|
"二": 2,
|
||
|
|
"两": 2,
|
||
|
|
"三": 3,
|
||
|
|
"四": 4,
|
||
|
|
"五": 5,
|
||
|
|
"六": 6,
|
||
|
|
"七": 7,
|
||
|
|
"八": 8,
|
||
|
|
"九": 9,
|
||
|
|
}
|
||
|
|
text = str(value or "").strip()
|
||
|
|
if not text:
|
||
|
|
return 0
|
||
|
|
if text == "十":
|
||
|
|
return 10
|
||
|
|
if "十" in text:
|
||
|
|
left, _, right = text.partition("十")
|
||
|
|
tens = digits.get(left, 1) if left else 1
|
||
|
|
ones = digits.get(right, 0) if right else 0
|
||
|
|
return tens * 10 + ones
|
||
|
|
return digits.get(text, 0)
|
||
|
|
|
||
|
|
|
||
|
|
def _parse_application_date(value: str) -> date | None:
|
||
|
|
normalized = str(value or "").strip().rstrip("日").replace("年", "-").replace("月", "-")
|
||
|
|
normalized = normalized.replace("/", "-").replace(".", "-")
|
||
|
|
parts = [part for part in normalized.split("-") if part]
|
||
|
|
if len(parts) != 3:
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
year, month, day = (int(part) for part in parts)
|
||
|
|
return date(year, month, day)
|
||
|
|
except ValueError:
|
||
|
|
return None
|