datetime arithmetic example code
These functions were originally written for Python 3.6+, then updated for 3.8+ and were not tested with other versions.
Functions implementing wall and absolute time semantics
from datetime import timedelta, timezone
UTC = timezone.utc
def wall_add(dt, other):
return dt + other
def wall_sub(dt, other):
if isinstance(other, timedelta):
return wall_add(dt, -1 * other)
return dt.replace(tzinfo=None)- other.replace(tzinfo=None)
def absolute_add(dt, other):
return (dt.astimezone(UTC) + other).astimezone(dt.tzinfo)
def absolute_sub(dt, other):
if isinstance(other, timedelta):
return absolute_add(dt, -1 * other)
return (dt.astimezone(UTC) - other.astimezone(UTC))
datetime subclasses with absolute or wall time semantics
from datetime import datetime, timedelta, timezone
UTC = timezone.utc
from functools import total_ordering
@total_ordering
class AbsoluteDateTime(datetime):
"""A version of datetime that uses only elapsed time semantics"""
_utc_datetime_cache = None
@property
def _utc_datetime(self):
if self._utc_datetime_cache is None:
dt = datetime(
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
self.microsecond,
tzinfo=self.tzinfo,
fold=self.fold,
)
self._utc_datetime_cache = dt.astimezone(UTC)
return self._utc_datetime_cache
def __add__(self, other):
# __add__ is only supported between datetime and timedelta
dt = datetime.__add__(self._utc_datetime, other)
if self.tzinfo is not UTC:
dt = dt.astimezone(self.tzinfo)
# Required to support the case where tzinfo is None
dt = dt.replace(tzinfo=self.tzinfo)
return type(self).as_absolute_datetime(dt)
def __sub__(self, other):
if isinstance(other, timedelta):
# Use __add__ implementation if it's datetime and timedelta
return self + (-1) * other
else:
return datetime.__sub__(self._utc_datetime, other.astimezone(UTC))
def __eq__(self, other):
return datetime.__eq__(self._utc_datetime, other.astimezone(UTC))
def __lt__(self, other):
return datetime.__lt__(self._utc_datetime, other.astimezone(UTC))
@classmethod
def as_absolute_datetime(cls, dt):
"""Construct an AbsoluteDatetime from any datetime subclass"""
return cls(
*dt.timetuple()[0:6],
microsecond=dt.microsecond,
tzinfo=dt.tzinfo,
fold=dt.fold
)
@total_ordering
class WallDateTime(datetime):
"""A version of datetime that uses only wall time semantics"""
def __add__(self, other):
# __add__ is only supported between datetime and timedelta
dt = datetime.__add__(self.replace(tzinfo=None), other)
if self.tzinfo is not None:
dt = dt.replace(tzinfo=self.tzinfo)
return self.__class__.as_wall_datetime(dt)
def __sub__(self, other):
if isinstance(other, timedelta):
# Use __add__ implementation if it's datetime and timedelta
return self + (-1) * other
else:
return datetime.__sub__(
self.replace(tzinfo=None), other.replace(tzinfo=None)
)
def __eq__(self, other):
return datetime.__eq__(
self.replace(tzinfo=None), other.replace(tzinfo=None)
)
def __lt__(self, other):
return datetime.__lt__(
self.replace(tzinfo=None), other.replace(tzinfo=None)
)
@classmethod
def as_wall_datetime(cls, dt):
"""Construct a WallDateTime from any datetime subclass"""
return cls(
*dt.timetuple()[0:6],
microsecond=dt.microsecond,
tzinfo=dt.tzinfo,
fold=dt.fold
)