Datetime range classes

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?

Sounds like something that might make a nice project on PyPI (assuming there isn’t something there already, and I assume there isn’t or you would have used that).

I haven’t actually used it, but DateTimeRange · PyPI is a thing