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