codekingpro/portable-devtools
114k
1# -*- coding: utf-8 -*-2import datetime3import calendar4 5import operator6from math import copysign7 8from six import integer_types9from warnings import warn10 11from ._common import weekday12 13MO, 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 18class relativedelta(object):19 """20 The relativedelta type is designed to be applied to an existing datetime and21 can replace specific components of that datetime, or represents an interval22 of time.23 24 It is based on the specification of the excellent work done by M.-A. Lemburg25 in his26 `mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.27 However, notice that this type does *NOT* implement the same algorithm as28 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. The31 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 a41 relativedelta with absolute information does not perform an arithmetic42 operation, but rather REPLACES the corresponding value in the43 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); adding47 or subtracting a relativedelta with relative information performs48 the corresponding arithmetic operation on the original datetime value49 with the information in the relativedelta.50 51 weekday:52 One of the weekday instances (MO, TU, etc) available in the53 relativedelta module. These instances may receive a parameter N,54 specifying the Nth weekday, which could be positive or negative55 (like MO(+1) or MO(-2)). Not specifying it is the same as specifying56 +1. You can also use an integer, where 0=MO. This argument is always57 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, use59 it in combination with the day argument (e.g. day=1, MO(1) for first60 Monday of the month).61 62 leapdays:63 Will add given days to the date found, if year is a leap64 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 keyword71 arguments. The plural is relative, and the singular is72 absolute. For each argument in the order below, the absolute form73 is applied first (by setting each attribute to that value) and74 then the relative form (by adding the value to the attribute).75 76 The order of attributes considered when this relativedelta is77 added to a datetime is:78 79 1. Year80 2. Month81 3. Day82 4. Hours83 5. Minutes84 6. Seconds85 7. Microseconds86 87 Finally, weekday is applied, using the rule described above.88 89 For example90 91 >>> from datetime import datetime92 >>> from dateutil.relativedelta import relativedelta, MO93 >>> dt = datetime(2018, 4, 9, 13, 37, 0)94 >>> delta = relativedelta(hours=25, day=1, weekday=MO(1))95 >>> dt + delta96 datetime.datetime(2018, 4, 2, 14, 37)97 98 First, the day is set to 1 (the first of the month), then 25 hours99 are added, to get to the 2nd day and 14th hour, finally the100 weekday is applied, but since the 2nd is already a Monday there is101 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 date114 if not (isinstance(dt1, datetime.date) and115 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 be119 # of the same type120 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 = 0128 self.months = 0129 self.days = 0130 self.leapdays = 0131 self.hours = 0132 self.minutes = 0133 self.seconds = 0134 self.microseconds = 0135 self.year = None136 self.month = None137 self.day = None138 self.weekday = None139 self.hour = None140 self.minute = None141 self.second = None142 self.microsecond = None143 self._has_time = 0144 145 # Get year / month delta between the two146 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-defined150 # time units (seconds, days and microseconds)151 dtm = self.__radd__(dt2)152 153 # If we've overshot our target, make an adjustment154 if dt1 < dt2:155 compare = operator.gt156 increment = 1157 else:158 compare = operator.lt159 increment = -1160 161 while compare(dt1, dtm):162 months += increment163 self._set_months(months)164 dtm = self.__radd__(dt2)165 166 # Get the timedelta between the "months-adjusted" date and dt1167 delta = dt1 - dtm168 self.seconds = delta.seconds + delta.days * 86400169 self.microseconds = delta.microseconds170 else:171 # Check for non-integer values in integer-only quantities172 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 information177 self.years = int(years)178 self.months = int(months)179 self.days = days + weeks * 7180 self.leapdays = leapdays181 self.hours = hours182 self.minutes = minutes183 self.seconds = seconds184 self.microseconds = microseconds185 186 # Absolute information187 self.year = year188 self.month = month189 self.day = day190 self.hour = hour191 self.minute = minute192 self.second = second193 self.microsecond = microsecond194 195 if any(x is not None and int(x) != x196 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 = weekday207 208 yday = 0209 if nlyearday:210 yday = nlyearday211 elif yearday:212 yday = yearday213 if yearday > 59:214 self.leapdays = -1215 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+1221 if idx == 0:222 self.day = yday223 else:224 self.day = yday-ydayidx[idx-1]225 break226 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 * s236 self.seconds += div * s237 if abs(self.seconds) > 59:238 s = _sign(self.seconds)239 div, mod = divmod(self.seconds * s, 60)240 self.seconds = mod * s241 self.minutes += div * s242 if abs(self.minutes) > 59:243 s = _sign(self.minutes)244 div, mod = divmod(self.minutes * s, 60)245 self.minutes = mod * s246 self.hours += div * s247 if abs(self.hours) > 23:248 s = _sign(self.hours)249 div, mod = divmod(self.hours * s, 24)250 self.hours = mod * s251 self.days += div * s252 if abs(self.months) > 11:253 s = _sign(self.months)254 div, mod = divmod(self.months * s, 12)255 self.months = mod * s256 self.years += div * s257 if (self.hours or self.minutes or self.seconds or self.microseconds258 or self.hour is not None or self.minute is not None or259 self.second is not None or self.microsecond is not None):260 self._has_time = 1261 else:262 self._has_time = 0263 264 @property265 def weeks(self):266 return int(self.days / 7.0)267 268 @weeks.setter269 def weeks(self, value):270 self.days = self.days - (self.weeks * 7) + value * 7271 272 def _set_months(self, months):273 self.months = months274 if abs(self.months) > 11:275 s = _sign(self.months)276 div, mod = divmod(self.months * s, 12)277 self.months = mod * s278 self.years = div * s279 else:280 self.years = 0281 282 def normalized(self):283 """284 Return a version of this object represented entirely using integer285 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 None329 else self.year),330 month=(other.month if other.month is not None331 else self.month),332 day=(other.day if other.day is not None333 else self.day),334 weekday=(other.weekday if other.weekday is not None335 else self.weekday),336 hour=(other.hour if other.hour is not None337 else self.hour),338 minute=(other.minute if other.minute is not None339 else self.minute),340 second=(other.second if other.second is not None341 else self.second),342 microsecond=(other.microsecond if other.microsecond343 is not None else344 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 NotImplemented364 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.years367 month = self.month or other.month368 if self.months:369 assert 1 <= abs(self.months) <= 12370 month += self.months371 if month > 12:372 year += 1373 month -= 12374 elif month < 1:375 year -= 1376 month += 12377 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] = value384 days = self.days385 if self.leapdays and month > 2 and calendar.isleap(year):386 days += self.leapdays387 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 1395 jumpdays = (abs(nth) - 1) * 7396 if nth > 0:397 jumpdays += (7 - ret.weekday() + weekday) % 7398 else:399 jumpdays += (ret.weekday() - weekday) % 7400 jumpdays *= -1401 ret += datetime.timedelta(days=jumpdays)402 return ret403 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 None422 else other.year),423 month=(self.month if self.month is not None else424 other.month),425 day=(self.day if self.day is not None else426 other.day),427 weekday=(self.weekday if self.weekday is not None else428 other.weekday),429 hour=(self.hour if self.hour is not None else430 other.hour),431 minute=(self.minute if self.minute is not None else432 other.minute),433 second=(self.second if self.second is not None else434 other.second),435 microsecond=(self.microsecond if self.microsecond436 is not None else437 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 and477 not self.months and478 not self.days and479 not self.hours and480 not self.minutes and481 not self.seconds and482 not self.microseconds and483 not self.leapdays and484 self.year is None and485 self.month is None and486 self.day is None and487 self.weekday is None and488 self.hour is None and489 self.minute is None and490 self.second is None and491 self.microsecond is None)492 # Compatibility with Python 2.x493 __nonzero__ = __bool__494 495 def __mul__(self, other):496 try:497 f = float(other)498 except TypeError:499 return NotImplemented500 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 NotImplemented523 if self.weekday or other.weekday:524 if not self.weekday or not other.weekday:525 return False526 if self.weekday.weekday != other.weekday.weekday:527 return False528 n1, n2 = self.weekday.n, other.weekday.n529 if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):530 return False531 return (self.years == other.years and532 self.months == other.months and533 self.days == other.days and534 self.hours == other.hours and535 self.minutes == other.minutes and536 self.seconds == other.seconds and537 self.microseconds == other.microseconds and538 self.leapdays == other.leapdays and539 self.year == other.year and540 self.month == other.month and541 self.day == other.day and542 self.hour == other.hour and543 self.minute == other.minute and544 self.second == other.second and545 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 NotImplemented575 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 596def _sign(x):597 return int(copysign(1, x))598 599# vim:ts=4:sw=4:et600 