mirror of https://github.com/zulip/zulip.git
152 lines
5.9 KiB
Python
152 lines
5.9 KiB
Python
"""monthdelta
|
|
|
|
Date calculation with months: MonthDelta class and monthmod() function.
|
|
"""
|
|
|
|
__all__ = ['MonthDelta', 'monthmod']
|
|
|
|
from datetime import date, timedelta
|
|
|
|
class MonthDelta(object):
|
|
"""Number of months offset from a date or datetime.
|
|
|
|
MonthDeltas allow date calculation without regard to the different lengths
|
|
of different months. A MonthDelta value added to a date produces another
|
|
date that has the same day-of-the-month, regardless of the lengths of the
|
|
intervening months. If the resulting date is in too short a month, the
|
|
last day in that month will result:
|
|
|
|
date(2008,1,30) + MonthDelta(1) -> date(2008,2,29)
|
|
|
|
MonthDeltas may be added, subtracted, multiplied, and floor-divided
|
|
similarly to timedeltas. They may not be added to timedeltas directly, as
|
|
both classes are intended to be used directly with dates and datetimes.
|
|
Only ints may be passed to the constructor. MonthDeltas are immutable.
|
|
|
|
NOTE: in calculations involving the 29th, 30th, and 31st days of the
|
|
month, MonthDeltas are not necessarily invertible [i.e., the result above
|
|
would not imply that date(2008,2,29) - MonthDelta(1) -> date(2008,1,30)].
|
|
"""
|
|
__slots__ = ('__months',)
|
|
|
|
def __init__(self, months=1):
|
|
if not isinstance(months, int):
|
|
raise TypeError('months must be an integer')
|
|
self.__months = months
|
|
def months(self):
|
|
return self.__months
|
|
months = property(months)
|
|
def __repr__(self):
|
|
try:
|
|
return 'MonthDelta({0})'.format(self.__months)
|
|
except AttributeError:
|
|
return 'MonthDelta(' + str(self.__months) + ')'
|
|
def __str__(self):
|
|
return str(self.__months) + ' month' + ((abs(self.__months) != 1
|
|
and 's') or '')
|
|
def __hash__(self):
|
|
return hash(self.__months)
|
|
def __eq__(self, other):
|
|
if isinstance(other, MonthDelta):
|
|
return (self.__months == other.months)
|
|
return False
|
|
def __ne__(self, other):
|
|
if isinstance(other, MonthDelta):
|
|
return (self.__months != other.months)
|
|
return True
|
|
def __lt__(self, other):
|
|
if isinstance(other, MonthDelta):
|
|
return (self.__months < other.months)
|
|
return NotImplemented
|
|
def __le__(self, other):
|
|
if isinstance(other, MonthDelta):
|
|
return (self.__months <= other.months)
|
|
return NotImplemented
|
|
def __gt__(self, other):
|
|
if isinstance(other, MonthDelta):
|
|
return (self.__months > other.months)
|
|
return NotImplemented
|
|
def __ge__(self, other):
|
|
if isinstance(other, MonthDelta):
|
|
return (self.__months >= other.months)
|
|
return NotImplemented
|
|
def __add__(self, other):
|
|
if isinstance(other, MonthDelta):
|
|
return MonthDelta(self.__months + other.months)
|
|
if isinstance(other, date):
|
|
day = other.day
|
|
# subract one because months are not zero-based
|
|
month = other.month + self.__months - 1
|
|
year = other.year + month // 12
|
|
# now add it back
|
|
month = month % 12 + 1
|
|
if month == 2:
|
|
if day >= 29 and not year%4 and (year%100 or not year%400):
|
|
day = 29
|
|
elif day > 28:
|
|
day = 28
|
|
elif month in (4, 6, 9, 11) and day > 30:
|
|
day = 30
|
|
try:
|
|
return other.replace(year, month, day)
|
|
except ValueError:
|
|
raise OverflowError('date value out of range')
|
|
return NotImplemented
|
|
def __sub__(self, other):
|
|
if isinstance(other, MonthDelta):
|
|
return MonthDelta(self.__months - other.months)
|
|
return NotImplemented
|
|
def __mul__(self, other):
|
|
if isinstance(other, int):
|
|
return MonthDelta(self.__months * other)
|
|
return NotImplemented
|
|
def __floordiv__(self, other):
|
|
# MonthDelta // MonthDelta -> int
|
|
if isinstance(other, MonthDelta):
|
|
return self.__months // other.months
|
|
if isinstance(other, int):
|
|
return MonthDelta(self.__months // other)
|
|
return NotImplemented
|
|
def __radd__(self, other):
|
|
return self + other
|
|
def __rsub__(self, other):
|
|
return -self + other
|
|
def __rmul__(self, other):
|
|
return self * other
|
|
def __ifloordiv__(self, other):
|
|
# in-place division by a MonthDelta (which will change the variable's
|
|
# type) is almost certainly a bug -- raising this error is the reason
|
|
# we don't just fall back on __floordiv__
|
|
if isinstance(other, MonthDelta):
|
|
raise TypeError('in-place division of a MonthDelta requires an '
|
|
'integer divisor')
|
|
if isinstance(other, int):
|
|
return MonthDelta(self.__months // other)
|
|
return NotImplemented
|
|
def __neg__(self):
|
|
return MonthDelta(-self.__months)
|
|
def __pos__(self):
|
|
return MonthDelta(+self.__months)
|
|
def __abs__(self):
|
|
return MonthDelta(abs(self.__months))
|
|
def __bool__(self):
|
|
return bool(self.__months)
|
|
__nonzero__ = __bool__
|
|
|
|
def monthmod(start, end):
|
|
"""Months between dates, plus leftover time.
|
|
|
|
Distribute the interim between start and end dates into MonthDelta and
|
|
timedelta portions. If and only if start is after end, returned MonthDelta
|
|
will be negative. Returned timedelta is always non-negative, and is always
|
|
smaller than the month in which the end date occurs.
|
|
|
|
Invariant: dt + monthmod(dt, dt+td)[0] + monthmod(dt, dt+td)[1] = dt + td
|
|
"""
|
|
if not (isinstance(start, date) and isinstance(end, date)):
|
|
raise TypeError('start and end must be dates')
|
|
md = MonthDelta(12*(end.year - start.year) + end.month - start.month -
|
|
int(start.day > end.day))
|
|
# will overflow (underflow?) for end near date.min
|
|
return md, end - (start + md)
|