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