I ran into a case where I needed to handle ranges of dates, but was surprised to learn that the datetime library didn’t include any such objects. So I started to write my own daterange object with the below:
"""DateRange Class"""
from typing import Union
from datetime import datetime, timedelta
from dataclasses import dataclass
@dataclass(order=True)
class DateRange:
"""Class for range of datetime objects"""
lower_date: Union[datetime, datetime.date]
upper_date: Union[datetime, datetime.date]
@classmethod
def strpdaterange(cls, date_range_string: str, format: str):
"""Creates DateRange object from 2 comma-delimeted dates """
try:
lower_date, upper_date = date_range_string.split(",")[0:2]
except ValueError as err:
err.add_note("Less than 2 dates found. In the comma-delimited string.")
raise
lower_date = datetime.strptime(lower_date, format)
upper_date = datetime.strptime(upper_date, format)
return cls(lower_date, upper_date)
def strftime(self, format: str):
"""Strings both lower_date and upper_date with a comma in between"""
return f"{self.lower_date.strftime(format)},{self.upper_date.strftime(format)}"
@staticmethod
def _convert_to_date(value: Union[datetime, datetime.date]) -> datetime.date:
"""Converts datetime object into date object"""
if isinstance(value, datetime):
value = value.date()
return value
@staticmethod
def _reverse_date_edges(lower: datetime.date, upper: datetime.date):
if lower > upper:
return upper, lower
return lower, upper
def move_upper_forward(self, value: timedelta):
"""Moves the upper_date value into the future"""
self.upper_date = self.upper_date + value
def move_lower_forward(self, value: timedelta):
"""Moves the lower_date value into the future"""
self.lower_date = self.lower_date + value
def move_upper_back(self, value: timedelta):
"""Moves the upper_date value into the past"""
self.move_upper_forward(-value)
def move_lower_back(self, value: timedelta):
"""Moves the lower_date value into the past"""
self.move_lower_forward(-value)
def __setattr__(self, attr: str, value: Union[datetime, datetime.date]) -> None:
print(value)
new_date = self._convert_to_date(value)
try:
if attr == "lower_date":
new_dates = (new_date, self.upper_date)
elif attr == "upper_date":
new_dates = (self.lower_date, new_date)
self.__dict__["lower_date"], self.__dict__["upper_date"] = self._reverse_date_edges(*new_dates)
except KeyError:
self.__dict__[attr] = new_date
def __getattr__(self, attr: str) -> Union[datetime.date, timedelta]:
if attr == "range":
if self.lower_date is not None and self.upper_date is not None:
return (self.upper_date - self.lower_date) + timedelta(1)
return timedelta(0)
return self.__dict__[attr]
def __contains__(self, date: Union[datetime, datetime.date]) -> bool:
date = self._convert_to_date(date)
return self.lower_date <= date <= self.upper_date
def __hash__(self) -> int:
return hash((self.lower_date, self.upper_date))
def __len__(self):
return abs(self.range.days)
def __str__(self) -> str:
return f"range from {self.lower_date} until {self.upper_date}"
today = datetime.today()
test = DateRange(today+timedelta(10), today+timedelta(5))
print(len(test))
test.move_lower_forward(timedelta(10))
print(test.lower_date, test.upper_date, test.range)
print(type(test.lower_date))
test1 = DateRange(today+timedelta(13), today+timedelta(8))
print(test1 < test)
I think this kind of class plus one for both general datetime and time ranges would be good to have in the standard library itself. Any thoughts?