zulip/tools/deprecated/finbot/monthdelta.py

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)