| """ |
| Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net> |
| |
| This module offers extensions to the standard Python |
| datetime module. |
| """ |
| __license__ = "Simplified BSD" |
| |
| import datetime |
| import calendar |
| |
| from six import integer_types |
| |
| __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] |
| |
| class weekday(object): |
| __slots__ = ["weekday", "n"] |
| |
| def __init__(self, weekday, n=None): |
| self.weekday = weekday |
| self.n = n |
| |
| def __call__(self, n): |
| if n == self.n: |
| return self |
| else: |
| return self.__class__(self.weekday, n) |
| |
| def __eq__(self, other): |
| try: |
| if self.weekday != other.weekday or self.n != other.n: |
| return False |
| except AttributeError: |
| return False |
| return True |
| |
| def __repr__(self): |
| s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] |
| if not self.n: |
| return s |
| else: |
| return "%s(%+d)" % (s, self.n) |
| |
| MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) |
| |
| class relativedelta(object): |
| """ |
| The relativedelta type is based on the specification of the excelent |
| work done by M.-A. Lemburg in his mx.DateTime extension. However, |
| notice that this type does *NOT* implement the same algorithm as |
| his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. |
| |
| There's two different ways to build a relativedelta instance. The |
| first one is passing it two date/datetime classes: |
| |
| relativedelta(datetime1, datetime2) |
| |
| And the other way is to use the following keyword arguments: |
| |
| year, month, day, hour, minute, second, microsecond: |
| Absolute information. |
| |
| years, months, weeks, days, hours, minutes, seconds, microseconds: |
| Relative information, may be negative. |
| |
| weekday: |
| One of the weekday instances (MO, TU, etc). These instances may |
| receive a parameter N, specifying the Nth weekday, which could |
| be positive or negative (like MO(+1) or MO(-2). Not specifying |
| it is the same as specifying +1. You can also use an integer, |
| where 0=MO. |
| |
| leapdays: |
| Will add given days to the date found, if year is a leap |
| year, and the date found is post 28 of february. |
| |
| yearday, nlyearday: |
| Set the yearday or the non-leap year day (jump leap days). |
| These are converted to day/month/leapdays information. |
| |
| Here is the behavior of operations with relativedelta: |
| |
| 1) Calculate the absolute year, using the 'year' argument, or the |
| original datetime year, if the argument is not present. |
| |
| 2) Add the relative 'years' argument to the absolute year. |
| |
| 3) Do steps 1 and 2 for month/months. |
| |
| 4) Calculate the absolute day, using the 'day' argument, or the |
| original datetime day, if the argument is not present. Then, |
| subtract from the day until it fits in the year and month |
| found after their operations. |
| |
| 5) Add the relative 'days' argument to the absolute day. Notice |
| that the 'weeks' argument is multiplied by 7 and added to |
| 'days'. |
| |
| 6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, |
| microsecond/microseconds. |
| |
| 7) If the 'weekday' argument is present, calculate the weekday, |
| with the given (wday, nth) tuple. wday is the index of the |
| weekday (0-6, 0=Mon), and nth is the number of weeks to add |
| forward or backward, depending on its signal. Notice that if |
| the calculated date is already Monday, for example, using |
| (0, 1) or (0, -1) won't change the day. |
| """ |
| |
| def __init__(self, dt1=None, dt2=None, |
| years=0, months=0, days=0, leapdays=0, weeks=0, |
| hours=0, minutes=0, seconds=0, microseconds=0, |
| year=None, month=None, day=None, weekday=None, |
| yearday=None, nlyearday=None, |
| hour=None, minute=None, second=None, microsecond=None): |
| if dt1 and dt2: |
| if (not isinstance(dt1, datetime.date)) or (not isinstance(dt2, datetime.date)): |
| raise TypeError("relativedelta only diffs datetime/date") |
| if not type(dt1) == type(dt2): #isinstance(dt1, type(dt2)): |
| if not isinstance(dt1, datetime.datetime): |
| dt1 = datetime.datetime.fromordinal(dt1.toordinal()) |
| elif not isinstance(dt2, datetime.datetime): |
| dt2 = datetime.datetime.fromordinal(dt2.toordinal()) |
| self.years = 0 |
| self.months = 0 |
| self.days = 0 |
| self.leapdays = 0 |
| self.hours = 0 |
| self.minutes = 0 |
| self.seconds = 0 |
| self.microseconds = 0 |
| self.year = None |
| self.month = None |
| self.day = None |
| self.weekday = None |
| self.hour = None |
| self.minute = None |
| self.second = None |
| self.microsecond = None |
| self._has_time = 0 |
| |
| months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month) |
| self._set_months(months) |
| dtm = self.__radd__(dt2) |
| if dt1 < dt2: |
| while dt1 > dtm: |
| months += 1 |
| self._set_months(months) |
| dtm = self.__radd__(dt2) |
| else: |
| while dt1 < dtm: |
| months -= 1 |
| self._set_months(months) |
| dtm = self.__radd__(dt2) |
| delta = dt1 - dtm |
| self.seconds = delta.seconds+delta.days*86400 |
| self.microseconds = delta.microseconds |
| else: |
| self.years = years |
| self.months = months |
| self.days = days+weeks*7 |
| self.leapdays = leapdays |
| self.hours = hours |
| self.minutes = minutes |
| self.seconds = seconds |
| self.microseconds = microseconds |
| self.year = year |
| self.month = month |
| self.day = day |
| self.hour = hour |
| self.minute = minute |
| self.second = second |
| self.microsecond = microsecond |
| |
| if isinstance(weekday, integer_types): |
| self.weekday = weekdays[weekday] |
| else: |
| self.weekday = weekday |
| |
| yday = 0 |
| if nlyearday: |
| yday = nlyearday |
| elif yearday: |
| yday = yearday |
| if yearday > 59: |
| self.leapdays = -1 |
| if yday: |
| ydayidx = [31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 366] |
| for idx, ydays in enumerate(ydayidx): |
| if yday <= ydays: |
| self.month = idx+1 |
| if idx == 0: |
| self.day = yday |
| else: |
| self.day = yday-ydayidx[idx-1] |
| break |
| else: |
| raise ValueError("invalid year day (%d)" % yday) |
| |
| self._fix() |
| |
| def _fix(self): |
| if abs(self.microseconds) > 999999: |
| s = self.microseconds//abs(self.microseconds) |
| div, mod = divmod(self.microseconds*s, 1000000) |
| self.microseconds = mod*s |
| self.seconds += div*s |
| if abs(self.seconds) > 59: |
| s = self.seconds//abs(self.seconds) |
| div, mod = divmod(self.seconds*s, 60) |
| self.seconds = mod*s |
| self.minutes += div*s |
| if abs(self.minutes) > 59: |
| s = self.minutes//abs(self.minutes) |
| div, mod = divmod(self.minutes*s, 60) |
| self.minutes = mod*s |
| self.hours += div*s |
| if abs(self.hours) > 23: |
| s = self.hours//abs(self.hours) |
| div, mod = divmod(self.hours*s, 24) |
| self.hours = mod*s |
| self.days += div*s |
| if abs(self.months) > 11: |
| s = self.months//abs(self.months) |
| div, mod = divmod(self.months*s, 12) |
| self.months = mod*s |
| self.years += div*s |
| if (self.hours or self.minutes or self.seconds or self.microseconds or |
| self.hour is not None or self.minute is not None or |
| self.second is not None or self.microsecond is not None): |
| self._has_time = 1 |
| else: |
| self._has_time = 0 |
| |
| def _set_months(self, months): |
| self.months = months |
| if abs(self.months) > 11: |
| s = self.months//abs(self.months) |
| div, mod = divmod(self.months*s, 12) |
| self.months = mod*s |
| self.years = div*s |
| else: |
| self.years = 0 |
| |
| def __add__(self, other): |
| if isinstance(other, relativedelta): |
| return relativedelta(years=other.years+self.years, |
| months=other.months+self.months, |
| days=other.days+self.days, |
| hours=other.hours+self.hours, |
| minutes=other.minutes+self.minutes, |
| seconds=other.seconds+self.seconds, |
| microseconds=other.microseconds+self.microseconds, |
| leapdays=other.leapdays or self.leapdays, |
| year=other.year or self.year, |
| month=other.month or self.month, |
| day=other.day or self.day, |
| weekday=other.weekday or self.weekday, |
| hour=other.hour or self.hour, |
| minute=other.minute or self.minute, |
| second=other.second or self.second, |
| microsecond=other.microsecond or self.microsecond) |
| if not isinstance(other, datetime.date): |
| raise TypeError("unsupported type for add operation") |
| elif self._has_time and not isinstance(other, datetime.datetime): |
| other = datetime.datetime.fromordinal(other.toordinal()) |
| year = (self.year or other.year)+self.years |
| month = self.month or other.month |
| if self.months: |
| assert 1 <= abs(self.months) <= 12 |
| month += self.months |
| if month > 12: |
| year += 1 |
| month -= 12 |
| elif month < 1: |
| year -= 1 |
| month += 12 |
| day = min(calendar.monthrange(year, month)[1], |
| self.day or other.day) |
| repl = {"year": year, "month": month, "day": day} |
| for attr in ["hour", "minute", "second", "microsecond"]: |
| value = getattr(self, attr) |
| if value is not None: |
| repl[attr] = value |
| days = self.days |
| if self.leapdays and month > 2 and calendar.isleap(year): |
| days += self.leapdays |
| ret = (other.replace(**repl) |
| + datetime.timedelta(days=days, |
| hours=self.hours, |
| minutes=self.minutes, |
| seconds=self.seconds, |
| microseconds=self.microseconds)) |
| if self.weekday: |
| weekday, nth = self.weekday.weekday, self.weekday.n or 1 |
| jumpdays = (abs(nth)-1)*7 |
| if nth > 0: |
| jumpdays += (7-ret.weekday()+weekday)%7 |
| else: |
| jumpdays += (ret.weekday()-weekday)%7 |
| jumpdays *= -1 |
| ret += datetime.timedelta(days=jumpdays) |
| return ret |
| |
| def __radd__(self, other): |
| return self.__add__(other) |
| |
| def __rsub__(self, other): |
| return self.__neg__().__radd__(other) |
| |
| def __sub__(self, other): |
| if not isinstance(other, relativedelta): |
| raise TypeError("unsupported type for sub operation") |
| return relativedelta(years=self.years-other.years, |
| months=self.months-other.months, |
| days=self.days-other.days, |
| hours=self.hours-other.hours, |
| minutes=self.minutes-other.minutes, |
| seconds=self.seconds-other.seconds, |
| microseconds=self.microseconds-other.microseconds, |
| leapdays=self.leapdays or other.leapdays, |
| year=self.year or other.year, |
| month=self.month or other.month, |
| day=self.day or other.day, |
| weekday=self.weekday or other.weekday, |
| hour=self.hour or other.hour, |
| minute=self.minute or other.minute, |
| second=self.second or other.second, |
| microsecond=self.microsecond or other.microsecond) |
| |
| def __neg__(self): |
| return relativedelta(years=-self.years, |
| months=-self.months, |
| days=-self.days, |
| hours=-self.hours, |
| minutes=-self.minutes, |
| seconds=-self.seconds, |
| microseconds=-self.microseconds, |
| leapdays=self.leapdays, |
| year=self.year, |
| month=self.month, |
| day=self.day, |
| weekday=self.weekday, |
| hour=self.hour, |
| minute=self.minute, |
| second=self.second, |
| microsecond=self.microsecond) |
| |
| def __bool__(self): |
| return not (not self.years and |
| not self.months and |
| not self.days and |
| not self.hours and |
| not self.minutes and |
| not self.seconds and |
| not self.microseconds and |
| not self.leapdays and |
| self.year is None and |
| self.month is None and |
| self.day is None and |
| self.weekday is None and |
| self.hour is None and |
| self.minute is None and |
| self.second is None and |
| self.microsecond is None) |
| |
| def __mul__(self, other): |
| f = float(other) |
| return relativedelta(years=int(self.years*f), |
| months=int(self.months*f), |
| days=int(self.days*f), |
| hours=int(self.hours*f), |
| minutes=int(self.minutes*f), |
| seconds=int(self.seconds*f), |
| microseconds=int(self.microseconds*f), |
| leapdays=self.leapdays, |
| year=self.year, |
| month=self.month, |
| day=self.day, |
| weekday=self.weekday, |
| hour=self.hour, |
| minute=self.minute, |
| second=self.second, |
| microsecond=self.microsecond) |
| |
| __rmul__ = __mul__ |
| |
| def __eq__(self, other): |
| if not isinstance(other, relativedelta): |
| return False |
| if self.weekday or other.weekday: |
| if not self.weekday or not other.weekday: |
| return False |
| if self.weekday.weekday != other.weekday.weekday: |
| return False |
| n1, n2 = self.weekday.n, other.weekday.n |
| if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): |
| return False |
| return (self.years == other.years and |
| self.months == other.months and |
| self.days == other.days and |
| self.hours == other.hours and |
| self.minutes == other.minutes and |
| self.seconds == other.seconds and |
| self.leapdays == other.leapdays and |
| self.year == other.year and |
| self.month == other.month and |
| self.day == other.day and |
| self.hour == other.hour and |
| self.minute == other.minute and |
| self.second == other.second and |
| self.microsecond == other.microsecond) |
| |
| def __ne__(self, other): |
| return not self.__eq__(other) |
| |
| def __div__(self, other): |
| return self.__mul__(1/float(other)) |
| |
| __truediv__ = __div__ |
| |
| def __repr__(self): |
| l = [] |
| for attr in ["years", "months", "days", "leapdays", |
| "hours", "minutes", "seconds", "microseconds"]: |
| value = getattr(self, attr) |
| if value: |
| l.append("%s=%+d" % (attr, value)) |
| for attr in ["year", "month", "day", "weekday", |
| "hour", "minute", "second", "microsecond"]: |
| value = getattr(self, attr) |
| if value is not None: |
| l.append("%s=%s" % (attr, repr(value))) |
| return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) |
| |
| # vim:ts=4:sw=4:et |