import datetime
from dateutil.relativedelta import relativedelta
from typing import Union
from functools import total_ordering
[docs]
class TimeInterval:
def __init__(
self,
value: int,
period: str
):
if not isinstance(value, int):
raise TypeError("Time interval must has a integer amount.")
if not isinstance(period, str) or len(period) != 1:
raise TypeError("Period of a time interval must be single character.")
self.value = value
self.period = period
[docs]
@classmethod
def from_string(cls, interval_string):
return cls(int(interval_string[:-1]), interval_string[-1])
[docs]
def __neg__(self):
return TimeInterval(-self.value, self.period)
[docs]
def __repr__(self) -> str:
return f"{self.value}{self.period}"
[docs]
def __mul__(self, other: int):
if isinstance(other, int):
return TimeInterval(self.value * other, self.period)
else:
raise TypeError("Time interval can only be multiplied by an integer.")
[docs]
def __rmul__(self, other: int):
return self.__mul__(other)
# with total_ordering decorator, we only need to implement __eq__ and __lt__
[docs]
@total_ordering
class Date:
MON = 0
TUE = 1
WED = 2
THU = 3
FRI = 4
SAT = 5
SUN = 6
def __init__(self, year: int, month: int, day: int) -> None:
self._date = datetime.date(year, month, day)
self.year = year
self.month = month
self.day = day
self.weekday = self._date.weekday()
[docs]
def to_tuple(self) -> tuple:
"""Return a tuple of (year, month, day)"""
return self._date.year, self._date.month, self._date.day
[docs]
@classmethod
def convert_from_datetime(cls, date: Union[datetime.date, 'Date']) -> 'Date':
"""Return a Date object from a datetime.date object"""
if isinstance(date, Date):
return date
return cls.from_datetime(date)
[docs]
@classmethod
def convert_from_datetimes(cls, dates: list[Union[datetime.date, 'Date']]) -> list['Date']:
"""Return a list of Date objects from a list of datetime.date objects"""
return [cls.convert_from_datetime(date) for date in dates]
[docs]
@classmethod
def from_datetime(cls, date: datetime.date) -> 'Date':
"""Return a Date object from a datetime.date object"""
return cls(date.year, date.month, date.day)
[docs]
@classmethod
def from_tuple(cls, date_tuple: tuple) -> 'Date':
"""Return a Date object from a tuple of (year, month, day)"""
return cls(date_tuple[0], date_tuple[1], date_tuple[2])
[docs]
def __sub__(self, other: object) -> int:
if isinstance(other, Date):
return (self._date - other._date).days
if isinstance(other, datetime.date):
return (self._date - other).days
raise TypeError(f"unsupported operand type(s) for -: 'Date' and '{type(other).__name__}'")
[docs]
def __add__(self, days: int) -> 'Date':
return self.add_days(days)
[docs]
def __eq__(self, other: object) -> bool:
if isinstance(other, Date):
return self._date == other._date
elif isinstance(other, datetime.date):
return self._date == other
else:
return False
[docs]
def __lt__(self, other: object) -> bool:
if isinstance(other, Date):
return self._date < other._date
elif isinstance(other, datetime.date):
return self._date < other
else:
return False
[docs]
def __repr__(self) -> str:
# return "Date({0}, {1:>2s}, {2:>2s})".format(self.year, str(self.month), str(self.day))
return f"{self._date.strftime('%a (%Y, %m, %d)')} - {self - datetime.date(1601, 1, 1)}"
@property
def is_weekend(self) -> bool:
"""Return True if the date is a weekend, False otherwise
Monday is 0 and Sunday is 6.
"""
return self.weekday >= 5
[docs]
def add_days(self, days: int) -> 'Date':
"""Return a new Date object by adding days to self"""
if isinstance(days, int) is False:
raise TypeError("days must be an integer")
return Date.from_datetime(self._date + datetime.timedelta(days=days))
[docs]
def add_months(self, months: int) -> 'Date':
"""Return a new Date object by adding months to self"""
if isinstance(months, int) is False:
raise TypeError("months must be an integer")
return Date.from_datetime(self._date + relativedelta(months=months))
[docs]
def add_weeks(self, weeks: int) -> 'Date':
"""Return a new Date object by adding weeks to self"""
if isinstance(weeks, int) is False:
raise TypeError("weeks must be an integer")
return Date.from_datetime(self._date + relativedelta(weeks=weeks))
[docs]
def add_years(self, years: int) -> 'Date':
"""Return a new Date object by adding years to self"""
if isinstance(years, int) is False:
raise TypeError("years must be an integer")
return Date.from_datetime(self._date + relativedelta(years=years))
[docs]
def add_tenor(self, tenor: str) -> 'Date':
"""Return a new Date object by adding tenor to self"""
if isinstance(tenor, str) is False:
raise TypeError("tenor must be a string")
if tenor[-1] == 'd' or tenor[-1] == 'D':
return self.add_days(int(tenor[:-1]))
elif tenor[-1] == 'w' or tenor[-1] == 'W':
return self.add_weeks(int(tenor[:-1]))
elif tenor[-1] == 'm' or tenor[-1] == 'M':
return self.add_months(int(tenor[:-1]))
elif tenor[-1] == 'y' or tenor[-1] == 'Y':
return self.add_years(int(tenor[:-1]))
else:
raise ValueError("tenor must be one of 'd', 'w', 'm', 'y'")
[docs]
def add_interval(self, time_interval: TimeInterval) -> 'Date':
return self.add_tenor(repr(time_interval))
[docs]
def strftime(self, fmt: str) -> str:
"""Return a string representing the date, controlled by an explicit format string"""
return self._date.strftime(fmt)
[docs]
def __hash__(self) -> int:
return hash(self._date)