Files
Neo-ZQYY/packages/shared/src/neozqyy_shared/datetime_utils.py

118 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""时区转换与日期范围工具。
默认时区Asia/ShanghaiUTC+8与业务数据库 timestamptz 对齐。
"""
import calendar
from datetime import datetime, date, timedelta
from dateutil import tz
SHANGHAI_TZ = tz.gettz("Asia/Shanghai")
def now_shanghai() -> datetime:
"""获取上海时区当前时间。"""
return datetime.now(SHANGHAI_TZ)
def date_range(start: date, end: date) -> list[date]:
"""生成日期范围列表(含首尾)。
start > end 时返回空列表。
"""
if start > end:
return []
days = (end - start).days + 1
return [start + timedelta(days=i) for i in range(days)]
# 营业日切点默认值(小时),与 .env BUSINESS_DAY_START_HOUR 对齐
DEFAULT_BUSINESS_DAY_START_HOUR = 8
def business_date(dt: datetime, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> date:
"""将时间戳归属到营业日。
day_start_hour 之前的记录归属前一天。
day_start_hour=8 时07:59 归属前一天08:00 归属当天。
"""
if dt.hour < day_start_hour:
return (dt - timedelta(days=1)).date()
return dt.date()
def business_month(dt: datetime, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> date:
"""将时间戳归属到营业月(返回该月第一天)。
当月1日 day_start_hour 之前的记录归属上月。
"""
biz_d = business_date(dt, day_start_hour)
return biz_d.replace(day=1)
def business_week_monday(dt: datetime, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> date:
"""将时间戳归属到营业周(返回该周周一日期)。
周一 day_start_hour 之前的记录归属上周。
"""
biz_d = business_date(dt, day_start_hour)
return biz_d - timedelta(days=biz_d.weekday())
def biz_date_sql_expr(col: str, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> str:
"""生成 PostgreSQL 营业日归属 SQL 表达式。
返回形如 DATE(col - INTERVAL '8 hours') 的字符串,
用于替换原来的 DATE(col)。
"""
return f"DATE({col} - INTERVAL '{day_start_hour} hours')"
def business_day_range(
biz_date: date, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR
) -> tuple[datetime, datetime]:
"""返回给定营业日的精确时间戳范围 [start, end)。
start = biz_date 当天 day_start_hour:00Asia/Shanghai
end = biz_date 次日 day_start_hour:00Asia/Shanghai
"""
start = datetime(biz_date.year, biz_date.month, biz_date.day,
day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ)
end = start + timedelta(days=1)
return (start, end)
def business_week_range(
week_monday: date, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR
) -> tuple[datetime, datetime]:
"""返回给定营业周(周一)的精确时间戳范围 [start, end)。
start = week_monday 当天 day_start_hour:00Asia/Shanghai
end = week_monday + 7天 day_start_hour:00Asia/Shanghai
"""
start = datetime(week_monday.year, week_monday.month, week_monday.day,
day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ)
end = start + timedelta(days=7)
return (start, end)
def business_month_range(
month_first: date, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR
) -> tuple[datetime, datetime]:
"""返回给定营业月(首日)的精确时间戳范围 [start, end)。
start = month_first 当天 day_start_hour:00Asia/Shanghai
end = 次月1日 day_start_hour:00Asia/Shanghai
"""
start = datetime(month_first.year, month_first.month, month_first.day,
day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ)
# 计算次月1日
if month_first.month == 12:
next_month_first = date(month_first.year + 1, 1, 1)
else:
next_month_first = date(month_first.year, month_first.month + 1, 1)
end = datetime(next_month_first.year, next_month_first.month,
next_month_first.day, day_start_hour, 0, 0,
tzinfo=SHANGHAI_TZ)
return (start, end)