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