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