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"(?P20\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