Home | History | Annotate | Download | only in dateutil
      1 # -*- coding: utf-8 -*-
      2 import datetime
      3 import calendar
      4 
      5 import operator
      6 from math import copysign
      7 
      8 from six import integer_types
      9 from warnings import warn
     10 
     11 from ._common import weekday
     12 
     13 MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
     14 
     15 __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
     16 
     17 
     18 class relativedelta(object):
     19     """
     20     The relativedelta type is based on the specification of the excellent
     21     work done by M.-A. Lemburg in his
     22     `mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
     23     However, notice that this type does *NOT* implement the same algorithm as
     24     his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
     25 
     26     There are two different ways to build a relativedelta instance. The
     27     first one is passing it two date/datetime classes::
     28 
     29         relativedelta(datetime1, datetime2)
     30 
     31     The second one is passing it any number of the following keyword arguments::
     32 
     33         relativedelta(arg1=x,arg2=y,arg3=z...)
     34 
     35         year, month, day, hour, minute, second, microsecond:
     36             Absolute information (argument is singular); adding or subtracting a
     37             relativedelta with absolute information does not perform an arithmetic
     38             operation, but rather REPLACES the corresponding value in the
     39             original datetime with the value(s) in relativedelta.
     40 
     41         years, months, weeks, days, hours, minutes, seconds, microseconds:
     42             Relative information, may be negative (argument is plural); adding
     43             or subtracting a relativedelta with relative information performs
     44             the corresponding aritmetic operation on the original datetime value
     45             with the information in the relativedelta.
     46 
     47         weekday:
     48             One of the weekday instances (MO, TU, etc). These instances may
     49             receive a parameter N, specifying the Nth weekday, which could
     50             be positive or negative (like MO(+1) or MO(-2). Not specifying
     51             it is the same as specifying +1. You can also use an integer,
     52             where 0=MO.
     53 
     54         leapdays:
     55             Will add given days to the date found, if year is a leap
     56             year, and the date found is post 28 of february.
     57 
     58         yearday, nlyearday:
     59             Set the yearday or the non-leap year day (jump leap days).
     60             These are converted to day/month/leapdays information.
     61 
     62     Here is the behavior of operations with relativedelta:
     63 
     64     1. Calculate the absolute year, using the 'year' argument, or the
     65        original datetime year, if the argument is not present.
     66 
     67     2. Add the relative 'years' argument to the absolute year.
     68 
     69     3. Do steps 1 and 2 for month/months.
     70 
     71     4. Calculate the absolute day, using the 'day' argument, or the
     72        original datetime day, if the argument is not present. Then,
     73        subtract from the day until it fits in the year and month
     74        found after their operations.
     75 
     76     5. Add the relative 'days' argument to the absolute day. Notice
     77        that the 'weeks' argument is multiplied by 7 and added to
     78        'days'.
     79 
     80     6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
     81        microsecond/microseconds.
     82 
     83     7. If the 'weekday' argument is present, calculate the weekday,
     84        with the given (wday, nth) tuple. wday is the index of the
     85        weekday (0-6, 0=Mon), and nth is the number of weeks to add
     86        forward or backward, depending on its signal. Notice that if
     87        the calculated date is already Monday, for example, using
     88        (0, 1) or (0, -1) won't change the day.
     89     """
     90 
     91     def __init__(self, dt1=None, dt2=None,
     92                  years=0, months=0, days=0, leapdays=0, weeks=0,
     93                  hours=0, minutes=0, seconds=0, microseconds=0,
     94                  year=None, month=None, day=None, weekday=None,
     95                  yearday=None, nlyearday=None,
     96                  hour=None, minute=None, second=None, microsecond=None):
     97 
     98         if dt1 and dt2:
     99             # datetime is a subclass of date. So both must be date
    100             if not (isinstance(dt1, datetime.date) and
    101                     isinstance(dt2, datetime.date)):
    102                 raise TypeError("relativedelta only diffs datetime/date")
    103 
    104             # We allow two dates, or two datetimes, so we coerce them to be
    105             # of the same type
    106             if (isinstance(dt1, datetime.datetime) !=
    107                     isinstance(dt2, datetime.datetime)):
    108                 if not isinstance(dt1, datetime.datetime):
    109                     dt1 = datetime.datetime.fromordinal(dt1.toordinal())
    110                 elif not isinstance(dt2, datetime.datetime):
    111                     dt2 = datetime.datetime.fromordinal(dt2.toordinal())
    112 
    113             self.years = 0
    114             self.months = 0
    115             self.days = 0
    116             self.leapdays = 0
    117             self.hours = 0
    118             self.minutes = 0
    119             self.seconds = 0
    120             self.microseconds = 0
    121             self.year = None
    122             self.month = None
    123             self.day = None
    124             self.weekday = None
    125             self.hour = None
    126             self.minute = None
    127             self.second = None
    128             self.microsecond = None
    129             self._has_time = 0
    130 
    131             # Get year / month delta between the two
    132             months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
    133             self._set_months(months)
    134 
    135             # Remove the year/month delta so the timedelta is just well-defined
    136             # time units (seconds, days and microseconds)
    137             dtm = self.__radd__(dt2)
    138 
    139             # If we've overshot our target, make an adjustment
    140             if dt1 < dt2:
    141                 compare = operator.gt
    142                 increment = 1
    143             else:
    144                 compare = operator.lt
    145                 increment = -1
    146 
    147             while compare(dt1, dtm):
    148                 months += increment
    149                 self._set_months(months)
    150                 dtm = self.__radd__(dt2)
    151 
    152             # Get the timedelta between the "months-adjusted" date and dt1
    153             delta = dt1 - dtm
    154             self.seconds = delta.seconds + delta.days * 86400
    155             self.microseconds = delta.microseconds
    156         else:
    157             # Check for non-integer values in integer-only quantities
    158             if any(x is not None and x != int(x) for x in (years, months)):
    159                 raise ValueError("Non-integer years and months are "
    160                                  "ambiguous and not currently supported.")
    161 
    162             # Relative information
    163             self.years = int(years)
    164             self.months = int(months)
    165             self.days = days + weeks * 7
    166             self.leapdays = leapdays
    167             self.hours = hours
    168             self.minutes = minutes
    169             self.seconds = seconds
    170             self.microseconds = microseconds
    171 
    172             # Absolute information
    173             self.year = year
    174             self.month = month
    175             self.day = day
    176             self.hour = hour
    177             self.minute = minute
    178             self.second = second
    179             self.microsecond = microsecond
    180 
    181             if any(x is not None and int(x) != x
    182                    for x in (year, month, day, hour,
    183                              minute, second, microsecond)):
    184                 # For now we'll deprecate floats - later it'll be an error.
    185                 warn("Non-integer value passed as absolute information. " +
    186                      "This is not a well-defined condition and will raise " +
    187                      "errors in future versions.", DeprecationWarning)
    188 
    189             if isinstance(weekday, integer_types):
    190                 self.weekday = weekdays[weekday]
    191             else:
    192                 self.weekday = weekday
    193 
    194             yday = 0
    195             if nlyearday:
    196                 yday = nlyearday
    197             elif yearday:
    198                 yday = yearday
    199                 if yearday > 59:
    200                     self.leapdays = -1
    201             if yday:
    202                 ydayidx = [31, 59, 90, 120, 151, 181, 212,
    203                            243, 273, 304, 334, 366]
    204                 for idx, ydays in enumerate(ydayidx):
    205                     if yday <= ydays:
    206                         self.month = idx+1
    207                         if idx == 0:
    208                             self.day = yday
    209                         else:
    210                             self.day = yday-ydayidx[idx-1]
    211                         break
    212                 else:
    213                     raise ValueError("invalid year day (%d)" % yday)
    214 
    215         self._fix()
    216 
    217     def _fix(self):
    218         if abs(self.microseconds) > 999999:
    219             s = _sign(self.microseconds)
    220             div, mod = divmod(self.microseconds * s, 1000000)
    221             self.microseconds = mod * s
    222             self.seconds += div * s
    223         if abs(self.seconds) > 59:
    224             s = _sign(self.seconds)
    225             div, mod = divmod(self.seconds * s, 60)
    226             self.seconds = mod * s
    227             self.minutes += div * s
    228         if abs(self.minutes) > 59:
    229             s = _sign(self.minutes)
    230             div, mod = divmod(self.minutes * s, 60)
    231             self.minutes = mod * s
    232             self.hours += div * s
    233         if abs(self.hours) > 23:
    234             s = _sign(self.hours)
    235             div, mod = divmod(self.hours * s, 24)
    236             self.hours = mod * s
    237             self.days += div * s
    238         if abs(self.months) > 11:
    239             s = _sign(self.months)
    240             div, mod = divmod(self.months * s, 12)
    241             self.months = mod * s
    242             self.years += div * s
    243         if (self.hours or self.minutes or self.seconds or self.microseconds
    244                 or self.hour is not None or self.minute is not None or
    245                 self.second is not None or self.microsecond is not None):
    246             self._has_time = 1
    247         else:
    248             self._has_time = 0
    249 
    250     @property
    251     def weeks(self):
    252         return int(self.days / 7.0)
    253 
    254     @weeks.setter
    255     def weeks(self, value):
    256         self.days = self.days - (self.weeks * 7) + value * 7
    257 
    258     def _set_months(self, months):
    259         self.months = months
    260         if abs(self.months) > 11:
    261             s = _sign(self.months)
    262             div, mod = divmod(self.months * s, 12)
    263             self.months = mod * s
    264             self.years = div * s
    265         else:
    266             self.years = 0
    267 
    268     def normalized(self):
    269         """
    270         Return a version of this object represented entirely using integer
    271         values for the relative attributes.
    272 
    273         >>> relativedelta(days=1.5, hours=2).normalized()
    274         relativedelta(days=1, hours=14)
    275 
    276         :return:
    277             Returns a :class:`dateutil.relativedelta.relativedelta` object.
    278         """
    279         # Cascade remainders down (rounding each to roughly nearest microsecond)
    280         days = int(self.days)
    281 
    282         hours_f = round(self.hours + 24 * (self.days - days), 11)
    283         hours = int(hours_f)
    284 
    285         minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
    286         minutes = int(minutes_f)
    287 
    288         seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
    289         seconds = int(seconds_f)
    290 
    291         microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
    292 
    293         # Constructor carries overflow back up with call to _fix()
    294         return self.__class__(years=self.years, months=self.months,
    295                               days=days, hours=hours, minutes=minutes,
    296                               seconds=seconds, microseconds=microseconds,
    297                               leapdays=self.leapdays, year=self.year,
    298                               month=self.month, day=self.day,
    299                               weekday=self.weekday, hour=self.hour,
    300                               minute=self.minute, second=self.second,
    301                               microsecond=self.microsecond)
    302 
    303     def __add__(self, other):
    304         if isinstance(other, relativedelta):
    305             return self.__class__(years=other.years + self.years,
    306                                  months=other.months + self.months,
    307                                  days=other.days + self.days,
    308                                  hours=other.hours + self.hours,
    309                                  minutes=other.minutes + self.minutes,
    310                                  seconds=other.seconds + self.seconds,
    311                                  microseconds=(other.microseconds +
    312                                                self.microseconds),
    313                                  leapdays=other.leapdays or self.leapdays,
    314                                  year=(other.year if other.year is not None
    315                                        else self.year),
    316                                  month=(other.month if other.month is not None
    317                                         else self.month),
    318                                  day=(other.day if other.day is not None
    319                                       else self.day),
    320                                  weekday=(other.weekday if other.weekday is not None
    321                                           else self.weekday),
    322                                  hour=(other.hour if other.hour is not None
    323                                        else self.hour),
    324                                  minute=(other.minute if other.minute is not None
    325                                          else self.minute),
    326                                  second=(other.second if other.second is not None
    327                                          else self.second),
    328                                  microsecond=(other.microsecond if other.microsecond
    329                                               is not None else
    330                                               self.microsecond))
    331         if isinstance(other, datetime.timedelta):
    332             return self.__class__(years=self.years,
    333                                   months=self.months,
    334                                   days=self.days + other.days,
    335                                   hours=self.hours,
    336                                   minutes=self.minutes,
    337                                   seconds=self.seconds + other.seconds,
    338                                   microseconds=self.microseconds + other.microseconds,
    339                                   leapdays=self.leapdays,
    340                                   year=self.year,
    341                                   month=self.month,
    342                                   day=self.day,
    343                                   weekday=self.weekday,
    344                                   hour=self.hour,
    345                                   minute=self.minute,
    346                                   second=self.second,
    347                                   microsecond=self.microsecond)
    348         if not isinstance(other, datetime.date):
    349             return NotImplemented
    350         elif self._has_time and not isinstance(other, datetime.datetime):
    351             other = datetime.datetime.fromordinal(other.toordinal())
    352         year = (self.year or other.year)+self.years
    353         month = self.month or other.month
    354         if self.months:
    355             assert 1 <= abs(self.months) <= 12
    356             month += self.months
    357             if month > 12:
    358                 year += 1
    359                 month -= 12
    360             elif month < 1:
    361                 year -= 1
    362                 month += 12
    363         day = min(calendar.monthrange(year, month)[1],
    364                   self.day or other.day)
    365         repl = {"year": year, "month": month, "day": day}
    366         for attr in ["hour", "minute", "second", "microsecond"]:
    367             value = getattr(self, attr)
    368             if value is not None:
    369                 repl[attr] = value
    370         days = self.days
    371         if self.leapdays and month > 2 and calendar.isleap(year):
    372             days += self.leapdays
    373         ret = (other.replace(**repl)
    374                + datetime.timedelta(days=days,
    375                                     hours=self.hours,
    376                                     minutes=self.minutes,
    377                                     seconds=self.seconds,
    378                                     microseconds=self.microseconds))
    379         if self.weekday:
    380             weekday, nth = self.weekday.weekday, self.weekday.n or 1
    381             jumpdays = (abs(nth) - 1) * 7
    382             if nth > 0:
    383                 jumpdays += (7 - ret.weekday() + weekday) % 7
    384             else:
    385                 jumpdays += (ret.weekday() - weekday) % 7
    386                 jumpdays *= -1
    387             ret += datetime.timedelta(days=jumpdays)
    388         return ret
    389 
    390     def __radd__(self, other):
    391         return self.__add__(other)
    392 
    393     def __rsub__(self, other):
    394         return self.__neg__().__radd__(other)
    395 
    396     def __sub__(self, other):
    397         if not isinstance(other, relativedelta):
    398             return NotImplemented   # In case the other object defines __rsub__
    399         return self.__class__(years=self.years - other.years,
    400                              months=self.months - other.months,
    401                              days=self.days - other.days,
    402                              hours=self.hours - other.hours,
    403                              minutes=self.minutes - other.minutes,
    404                              seconds=self.seconds - other.seconds,
    405                              microseconds=self.microseconds - other.microseconds,
    406                              leapdays=self.leapdays or other.leapdays,
    407                              year=(self.year if self.year is not None
    408                                    else other.year),
    409                              month=(self.month if self.month is not None else
    410                                     other.month),
    411                              day=(self.day if self.day is not None else
    412                                   other.day),
    413                              weekday=(self.weekday if self.weekday is not None else
    414                                       other.weekday),
    415                              hour=(self.hour if self.hour is not None else
    416                                    other.hour),
    417                              minute=(self.minute if self.minute is not None else
    418                                      other.minute),
    419                              second=(self.second if self.second is not None else
    420                                      other.second),
    421                              microsecond=(self.microsecond if self.microsecond
    422                                           is not None else
    423                                           other.microsecond))
    424 
    425     def __abs__(self):
    426         return self.__class__(years=abs(self.years),
    427                               months=abs(self.months),
    428                               days=abs(self.days),
    429                               hours=abs(self.hours),
    430                               minutes=abs(self.minutes),
    431                               seconds=abs(self.seconds),
    432                               microseconds=abs(self.microseconds),
    433                               leapdays=self.leapdays,
    434                               year=self.year,
    435                               month=self.month,
    436                               day=self.day,
    437                               weekday=self.weekday,
    438                               hour=self.hour,
    439                               minute=self.minute,
    440                               second=self.second,
    441                               microsecond=self.microsecond)
    442 
    443     def __neg__(self):
    444         return self.__class__(years=-self.years,
    445                              months=-self.months,
    446                              days=-self.days,
    447                              hours=-self.hours,
    448                              minutes=-self.minutes,
    449                              seconds=-self.seconds,
    450                              microseconds=-self.microseconds,
    451                              leapdays=self.leapdays,
    452                              year=self.year,
    453                              month=self.month,
    454                              day=self.day,
    455                              weekday=self.weekday,
    456                              hour=self.hour,
    457                              minute=self.minute,
    458                              second=self.second,
    459                              microsecond=self.microsecond)
    460 
    461     def __bool__(self):
    462         return not (not self.years and
    463                     not self.months and
    464                     not self.days and
    465                     not self.hours and
    466                     not self.minutes and
    467                     not self.seconds and
    468                     not self.microseconds and
    469                     not self.leapdays and
    470                     self.year is None and
    471                     self.month is None and
    472                     self.day is None and
    473                     self.weekday is None and
    474                     self.hour is None and
    475                     self.minute is None and
    476                     self.second is None and
    477                     self.microsecond is None)
    478     # Compatibility with Python 2.x
    479     __nonzero__ = __bool__
    480 
    481     def __mul__(self, other):
    482         try:
    483             f = float(other)
    484         except TypeError:
    485             return NotImplemented
    486 
    487         return self.__class__(years=int(self.years * f),
    488                              months=int(self.months * f),
    489                              days=int(self.days * f),
    490                              hours=int(self.hours * f),
    491                              minutes=int(self.minutes * f),
    492                              seconds=int(self.seconds * f),
    493                              microseconds=int(self.microseconds * f),
    494                              leapdays=self.leapdays,
    495                              year=self.year,
    496                              month=self.month,
    497                              day=self.day,
    498                              weekday=self.weekday,
    499                              hour=self.hour,
    500                              minute=self.minute,
    501                              second=self.second,
    502                              microsecond=self.microsecond)
    503 
    504     __rmul__ = __mul__
    505 
    506     def __eq__(self, other):
    507         if not isinstance(other, relativedelta):
    508             return NotImplemented
    509         if self.weekday or other.weekday:
    510             if not self.weekday or not other.weekday:
    511                 return False
    512             if self.weekday.weekday != other.weekday.weekday:
    513                 return False
    514             n1, n2 = self.weekday.n, other.weekday.n
    515             if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
    516                 return False
    517         return (self.years == other.years and
    518                 self.months == other.months and
    519                 self.days == other.days and
    520                 self.hours == other.hours and
    521                 self.minutes == other.minutes and
    522                 self.seconds == other.seconds and
    523                 self.microseconds == other.microseconds and
    524                 self.leapdays == other.leapdays and
    525                 self.year == other.year and
    526                 self.month == other.month and
    527                 self.day == other.day and
    528                 self.hour == other.hour and
    529                 self.minute == other.minute and
    530                 self.second == other.second and
    531                 self.microsecond == other.microsecond)
    532 
    533     def __hash__(self):
    534         return hash((
    535             self.weekday,
    536             self.years,
    537             self.months,
    538             self.days,
    539             self.hours,
    540             self.minutes,
    541             self.seconds,
    542             self.microseconds,
    543             self.leapdays,
    544             self.year,
    545             self.month,
    546             self.day,
    547             self.hour,
    548             self.minute,
    549             self.second,
    550             self.microsecond,
    551         ))
    552 
    553     def __ne__(self, other):
    554         return not self.__eq__(other)
    555 
    556     def __div__(self, other):
    557         try:
    558             reciprocal = 1 / float(other)
    559         except TypeError:
    560             return NotImplemented
    561 
    562         return self.__mul__(reciprocal)
    563 
    564     __truediv__ = __div__
    565 
    566     def __repr__(self):
    567         l = []
    568         for attr in ["years", "months", "days", "leapdays",
    569                      "hours", "minutes", "seconds", "microseconds"]:
    570             value = getattr(self, attr)
    571             if value:
    572                 l.append("{attr}={value:+g}".format(attr=attr, value=value))
    573         for attr in ["year", "month", "day", "weekday",
    574                      "hour", "minute", "second", "microsecond"]:
    575             value = getattr(self, attr)
    576             if value is not None:
    577                 l.append("{attr}={value}".format(attr=attr, value=repr(value)))
    578         return "{classname}({attrs})".format(classname=self.__class__.__name__,
    579                                              attrs=", ".join(l))
    580 
    581 
    582 def _sign(x):
    583     return int(copysign(1, x))
    584 
    585 # vim:ts=4:sw=4:et
    586