date_parser.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. """
  2. 日期解析工具
  3. 支持多种自然语言日期格式解析,包括相对日期和绝对日期。
  4. """
  5. import re
  6. from datetime import datetime, timedelta
  7. from .errors import InvalidParameterError
  8. class DateParser:
  9. """日期解析器类"""
  10. # 中文日期映射
  11. CN_DATE_MAPPING = {
  12. "今天": 0,
  13. "昨天": 1,
  14. "前天": 2,
  15. "大前天": 3,
  16. }
  17. # 英文日期映射
  18. EN_DATE_MAPPING = {
  19. "today": 0,
  20. "yesterday": 1,
  21. }
  22. # 星期映射
  23. WEEKDAY_CN = {
  24. "一": 0, "二": 1, "三": 2, "四": 3,
  25. "五": 4, "六": 5, "日": 6, "天": 6
  26. }
  27. WEEKDAY_EN = {
  28. "monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3,
  29. "friday": 4, "saturday": 5, "sunday": 6
  30. }
  31. @staticmethod
  32. def parse_date_query(date_query: str) -> datetime:
  33. """
  34. 解析日期查询字符串
  35. 支持的格式:
  36. - 相对日期(中文):今天、昨天、前天、大前天、N天前
  37. - 相对日期(英文):today、yesterday、N days ago
  38. - 星期(中文):上周一、上周二、本周三
  39. - 星期(英文):last monday、this friday
  40. - 绝对日期:2025-10-10、10月10日、2025年10月10日
  41. Args:
  42. date_query: 日期查询字符串
  43. Returns:
  44. datetime对象
  45. Raises:
  46. InvalidParameterError: 日期格式无法识别
  47. Examples:
  48. >>> DateParser.parse_date_query("今天")
  49. datetime(2025, 10, 11)
  50. >>> DateParser.parse_date_query("昨天")
  51. datetime(2025, 10, 10)
  52. >>> DateParser.parse_date_query("3天前")
  53. datetime(2025, 10, 8)
  54. >>> DateParser.parse_date_query("2025-10-10")
  55. datetime(2025, 10, 10)
  56. """
  57. if not date_query or not isinstance(date_query, str):
  58. raise InvalidParameterError(
  59. "日期查询字符串不能为空",
  60. suggestion="请提供有效的日期查询,如:今天、昨天、2025-10-10"
  61. )
  62. date_query = date_query.strip().lower()
  63. # 1. 尝试解析中文常用相对日期
  64. if date_query in DateParser.CN_DATE_MAPPING:
  65. days_ago = DateParser.CN_DATE_MAPPING[date_query]
  66. return datetime.now() - timedelta(days=days_ago)
  67. # 2. 尝试解析英文常用相对日期
  68. if date_query in DateParser.EN_DATE_MAPPING:
  69. days_ago = DateParser.EN_DATE_MAPPING[date_query]
  70. return datetime.now() - timedelta(days=days_ago)
  71. # 3. 尝试解析 "N天前" 或 "N days ago"
  72. cn_days_ago_match = re.match(r'(\d+)\s*天前', date_query)
  73. if cn_days_ago_match:
  74. days = int(cn_days_ago_match.group(1))
  75. if days > 365:
  76. raise InvalidParameterError(
  77. f"天数过大: {days}天",
  78. suggestion="请使用小于365天的相对日期或使用绝对日期"
  79. )
  80. return datetime.now() - timedelta(days=days)
  81. en_days_ago_match = re.match(r'(\d+)\s*days?\s+ago', date_query)
  82. if en_days_ago_match:
  83. days = int(en_days_ago_match.group(1))
  84. if days > 365:
  85. raise InvalidParameterError(
  86. f"天数过大: {days}天",
  87. suggestion="请使用小于365天的相对日期或使用绝对日期"
  88. )
  89. return datetime.now() - timedelta(days=days)
  90. # 4. 尝试解析星期(中文):上周一、本周三
  91. cn_weekday_match = re.match(r'(上|本)周([一二三四五六日天])', date_query)
  92. if cn_weekday_match:
  93. week_type = cn_weekday_match.group(1) # 上 或 本
  94. weekday_str = cn_weekday_match.group(2)
  95. target_weekday = DateParser.WEEKDAY_CN[weekday_str]
  96. return DateParser._get_date_by_weekday(target_weekday, week_type == "上")
  97. # 5. 尝试解析星期(英文):last monday、this friday
  98. en_weekday_match = re.match(r'(last|this)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)', date_query)
  99. if en_weekday_match:
  100. week_type = en_weekday_match.group(1) # last 或 this
  101. weekday_str = en_weekday_match.group(2)
  102. target_weekday = DateParser.WEEKDAY_EN[weekday_str]
  103. return DateParser._get_date_by_weekday(target_weekday, week_type == "last")
  104. # 6. 尝试解析绝对日期:YYYY-MM-DD
  105. iso_date_match = re.match(r'(\d{4})-(\d{1,2})-(\d{1,2})', date_query)
  106. if iso_date_match:
  107. year = int(iso_date_match.group(1))
  108. month = int(iso_date_match.group(2))
  109. day = int(iso_date_match.group(3))
  110. try:
  111. return datetime(year, month, day)
  112. except ValueError as e:
  113. raise InvalidParameterError(
  114. f"无效的日期: {date_query}",
  115. suggestion=f"日期值错误: {str(e)}"
  116. )
  117. # 7. 尝试解析中文日期:MM月DD日 或 YYYY年MM月DD日
  118. cn_date_match = re.match(r'(?:(\d{4})年)?(\d{1,2})月(\d{1,2})日', date_query)
  119. if cn_date_match:
  120. year_str = cn_date_match.group(1)
  121. month = int(cn_date_match.group(2))
  122. day = int(cn_date_match.group(3))
  123. # 如果没有年份,使用当前年份
  124. if year_str:
  125. year = int(year_str)
  126. else:
  127. year = datetime.now().year
  128. # 如果月份大于当前月份,说明是去年
  129. current_month = datetime.now().month
  130. if month > current_month:
  131. year -= 1
  132. try:
  133. return datetime(year, month, day)
  134. except ValueError as e:
  135. raise InvalidParameterError(
  136. f"无效的日期: {date_query}",
  137. suggestion=f"日期值错误: {str(e)}"
  138. )
  139. # 8. 尝试解析斜杠格式:YYYY/MM/DD 或 MM/DD
  140. slash_date_match = re.match(r'(?:(\d{4})/)?(\d{1,2})/(\d{1,2})', date_query)
  141. if slash_date_match:
  142. year_str = slash_date_match.group(1)
  143. month = int(slash_date_match.group(2))
  144. day = int(slash_date_match.group(3))
  145. if year_str:
  146. year = int(year_str)
  147. else:
  148. year = datetime.now().year
  149. current_month = datetime.now().month
  150. if month > current_month:
  151. year -= 1
  152. try:
  153. return datetime(year, month, day)
  154. except ValueError as e:
  155. raise InvalidParameterError(
  156. f"无效的日期: {date_query}",
  157. suggestion=f"日期值错误: {str(e)}"
  158. )
  159. # 如果所有格式都不匹配
  160. raise InvalidParameterError(
  161. f"无法识别的日期格式: {date_query}",
  162. suggestion=(
  163. "支持的格式:\n"
  164. "- 相对日期: 今天、昨天、前天、3天前、today、yesterday、3 days ago\n"
  165. "- 星期: 上周一、本周三、last monday、this friday\n"
  166. "- 绝对日期: 2025-10-10、10月10日、2025年10月10日"
  167. )
  168. )
  169. @staticmethod
  170. def _get_date_by_weekday(target_weekday: int, is_last_week: bool) -> datetime:
  171. """
  172. 根据星期几获取日期
  173. Args:
  174. target_weekday: 目标星期 (0=周一, 6=周日)
  175. is_last_week: 是否是上周
  176. Returns:
  177. datetime对象
  178. """
  179. today = datetime.now()
  180. current_weekday = today.weekday()
  181. # 计算天数差
  182. if is_last_week:
  183. # 上周的某一天
  184. days_diff = current_weekday - target_weekday + 7
  185. else:
  186. # 本周的某一天
  187. days_diff = current_weekday - target_weekday
  188. if days_diff < 0:
  189. days_diff += 7
  190. return today - timedelta(days=days_diff)
  191. @staticmethod
  192. def format_date_folder(date: datetime) -> str:
  193. """
  194. 将日期格式化为文件夹名称
  195. Args:
  196. date: datetime对象
  197. Returns:
  198. 文件夹名称,格式: YYYY年MM月DD日
  199. Examples:
  200. >>> DateParser.format_date_folder(datetime(2025, 10, 11))
  201. '2025年10月11日'
  202. """
  203. return date.strftime("%Y年%m月%d日")
  204. @staticmethod
  205. def validate_date_not_future(date: datetime) -> None:
  206. """
  207. 验证日期不在未来
  208. Args:
  209. date: 待验证的日期
  210. Raises:
  211. InvalidParameterError: 日期在未来
  212. """
  213. if date.date() > datetime.now().date():
  214. raise InvalidParameterError(
  215. f"不能查询未来的日期: {date.strftime('%Y-%m-%d')}",
  216. suggestion="请使用今天或过去的日期"
  217. )
  218. @staticmethod
  219. def validate_date_not_too_old(date: datetime, max_days: int = 365) -> None:
  220. """
  221. 验证日期不太久远
  222. Args:
  223. date: 待验证的日期
  224. max_days: 最大天数
  225. Raises:
  226. InvalidParameterError: 日期太久远
  227. """
  228. days_ago = (datetime.now().date() - date.date()).days
  229. if days_ago > max_days:
  230. raise InvalidParameterError(
  231. f"日期太久远: {date.strftime('%Y-%m-%d')} ({days_ago}天前)",
  232. suggestion=f"请查询{max_days}天内的数据"
  233. )