certflow.utils.date_utils 源代码

"""日期处理工具模块

提供全局统一的日期格式化和提取功能。

分层约束:本模块属 utils 层,**不得依赖 PySide6**,须保证在无 Qt
环境下可导入。原先内置的 QDate 构造/解析工具已上移至
``certflow.views.common.qdate_utils``,此处仅保留向后兼容垫片。
"""

from __future__ import annotations

import contextlib
from datetime import datetime
from typing import Any

import pandas as pd

# Qt 的最小有效日期(格里高利历在英美国家的起点),
# 也是 QDateEdit.setDate() 收到无效 QDate 时的“回退零日期”。
# 任何日期解析都绝不能静默回退到这个值。
QT_MIN_VALID_DATE = (1752, 9, 14)


[文档] def safe_qdate(year: int, month: int, day: int) -> Any | None: """向后兼容垫片:转发至 ``views.common.qdate_utils.safe_qdate`` Deprecated: 新代码应直接从 ``certflow.views.common.qdate_utils`` 导入。 本垫片仅为兼容存量调用点保留,在无 PySide6 环境调用会抛 ImportError。 Args: year: 年份 month: 月份 (1-12) day: 日 (1-31,需符合该月实际天数) Returns: QDate 实例(有效),或 None(参数非法时) """ from certflow.views.common.qdate_utils import safe_qdate as _impl return _impl(year, month, day)
[文档] def parse_qdate(text: str, fmt: str = "yyyy-MM-dd") -> Any | None: """向后兼容垫片:转发至 ``views.common.qdate_utils.parse_qdate`` Deprecated: 新代码应直接从 ``certflow.views.common.qdate_utils`` 导入。 本垫片仅为兼容存量调用点保留,在无 PySide6 环境调用会抛 ImportError。 Args: text: 日期字符串,如 "2026-07-01" fmt: Qt 日期格式,默认 "yyyy-MM-dd" Returns: QDate 实例(有效),或 None(解析失败时) """ from certflow.views.common.qdate_utils import parse_qdate as _impl return _impl(text, fmt)
# 支持的日期格式列表(按优先级排列) _DATE_FORMATS = [ "%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y/%m/%d", "%Y.%m.%d", "%d-%m-%Y", "%d/%m/%Y", "%Y%m%d", "%y%m%d", ] def _is_empty_or_placeholder(date_value: Any) -> bool: """检查日期值是否为空值或占位符。""" if date_value is None: return True s = date_value.strip() if isinstance(date_value, str) else str(date_value).strip() return not s or s.lower() == "nan" or "【空白" in s def _try_parse_date(value_str: str) -> str | None: """尝试用预定义格式列表解析日期字符串,返回 YYYY-MM-DD 或 None。""" for fmt in _DATE_FORMATS: try: return datetime.strptime(value_str, fmt).strftime("%Y-%m-%d") except ValueError: continue return None def _is_already_formatted(value_str: str) -> bool: """检查字符串是否已经是 YYYY-MM-DD 格式。""" return len(value_str) >= 10 and value_str[4] == "-" and value_str[7] == "-"
[文档] def normalize_plan_date(date_value: Any) -> str | None: """将日期值统一格式化为 YYYY-MM-DD 标准格式(不带时分秒)。 全局统一的日期格式化方法,所有模块导入 plan_date 时均应调用此方法。 支持格式: - datetime / pandas.Timestamp 对象 - 字符串: 2024-01-15 14:30:00, 2024-01-15, 2024/01/15, 2024.01.15, 15-01-2024, 15/01/2024, 20240115, 240115 Args: date_value: 原始日期值,可以是 datetime、Timestamp、字符串或 None Returns: str | None: 格式化后的 YYYY-MM-DD 日期字符串, 如果值为空、无效或无法解析,返回 None """ if _is_empty_or_placeholder(date_value): return None with contextlib.suppress(Exception): # datetime / Timestamp 对象 if isinstance(date_value, (datetime | pd.Timestamp)): return date_value.strftime("%Y-%m-%d") value_str = str(date_value).strip() # 已是标准格式,直接返回 if _is_already_formatted(value_str): return value_str[:10] # 尝试预定义格式解析 result = _try_parse_date(value_str) if result: return result # 兜底:含 '-' 分隔符则截取前 10 位 if "-" in value_str: return value_str[:10] return None
[文档] def extract_yymm_from_date(plan_date: Any) -> str | None: """从计划日期提取 YYMM 格式(后两位年份+两位月份) 先通过 normalize_plan_date 统一格式化为 YYYY-MM-DD, 再从中提取年份后两位和月份。 Args: plan_date: 日期对象或日期字符串(支持任意 normalize_plan_date 支持的格式) Returns: Optional[str]: 格式如 "2603" 的 YYMM 字符串,失败返回 None """ normalized = normalize_plan_date(plan_date) if not normalized: return None try: date_obj = datetime.strptime(normalized, "%Y-%m-%d") except ValueError: return None year = date_obj.year % 100 # 后两位 month = date_obj.month # 有效性校验 if not (1 <= month <= 12): return None if not (0 <= year <= 99): return None return f"{year:02d}{month:02d}"
[文档] def extract_yymm_int_from_date(plan_date: Any) -> int | None: """从计划日期提取 YYMM 整数格式 Args: plan_date: 日期对象或日期字符串 Returns: Optional[int]: 如 2603 的整数,失败返回 None """ yymm_str = extract_yymm_from_date(plan_date) return int(yymm_str) if yymm_str else None