Files
X-Financial/server/src/app/services/user_agent_application_dates.py

143 lines
4.2 KiB
Python
Raw Normal View History

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_application_days_from_time_range(time_text: str) -> int:
matches = re.findall(
r"20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?",
str(time_text or ""),
)
if len(matches) < 2:
return 0
start_date = _parse_application_date(matches[0])
end_date = _parse_application_date(matches[-1])
if start_date is None or end_date is None or end_date < start_date:
return 0
return (end_date - start_date).days + 1
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