1# -*- coding: utf-8 -*-
2import datetime
3import calendar
4
5import operator
6from math import copysign
7
8from six import integer_types
9from warnings import warn
10
11from ._common import weekday
12
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 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
596def _sign(x):
597 return int(copysign(1, x))
598
599# vim:ts=4:sw=4:et