"""Datetime rounding and timezone utilities.
Provides helpers that coerce :class:`datetime.datetime` and
:class:`datetime.date` objects to useful calendar boundaries (week, month)
and to UTC midnight, without depending on any third-party libraries.
Example:
>>> from bolster.utils.dt import round_to_week, round_to_month
>>> from datetime import date
>>> round_to_week(date(2024, 3, 13)) # Wednesday → previous Monday
datetime.date(2024, 3, 11)
>>> round_to_month(date(2024, 3, 13))
datetime.date(2024, 3, 1)
"""
from datetime import date, datetime, timedelta, timezone
[docs]
def round_to_week(dt: datetime | date) -> date:
"""Return a date for the Monday of the week containing the given date.
Args:
dt: A :class:`datetime.datetime` or :class:`datetime.date` to round.
Returns:
The Monday on or before ``dt`` as a :class:`datetime.date`.
Example:
>>> round_to_week(datetime(2018,8,9,12,1))
datetime.date(2018, 8, 6)
>>> round_to_week(date(2018,8,9))
datetime.date(2018, 8, 6)
"""
return (dt.date() if isinstance(dt, datetime) else dt) - timedelta(days=dt.weekday())
[docs]
def round_to_month(dt: datetime | date) -> date:
"""Return a date for the first day of the month containing the given date.
Args:
dt: A :class:`datetime.datetime` or :class:`datetime.date` to round.
Returns:
The first day of the month of ``dt`` as a :class:`datetime.date`.
Example:
>>> round_to_month(datetime(2018,8,9,12,1))
datetime.date(2018, 8, 1)
>>> round_to_month(date(2018,8,9))
datetime.date(2018, 8, 1)
"""
dt = dt.date() if isinstance(dt, datetime) else dt
return date(dt.year, dt.month, 1)
[docs]
def utc_midnight_on(dt: datetime) -> datetime:
"""Return UTC midnight for the calendar date of a given datetime.
Converts any :class:`datetime.datetime` to ``00:00:00 UTC`` on the same
calendar date, regardless of the original time or timezone offset. This
is useful when a downstream service requires UTC-naive timestamps or
rejects sub-day granularity.
Args:
dt: A :class:`datetime.datetime` whose calendar date is used. The
time component and any timezone info are ignored.
Returns:
A timezone-aware :class:`datetime.datetime` at ``00:00:00 UTC`` on
the same calendar date as ``dt``.
Example:
>>> utc_midnight_on(datetime(2018,9,1,12,12))
datetime.datetime(2018, 9, 1, 0, 0, tzinfo=datetime.timezone.utc)
>>> utc_midnight_on(datetime(2018,9,1,12,12, tzinfo=timezone(timedelta(hours=-13))))
datetime.datetime(2018, 9, 1, 0, 0, tzinfo=datetime.timezone.utc)
"""
return datetime.combine(dt.date(), datetime.min.time()).replace(tzinfo=timezone.utc)