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 if yearday > 59:
212 self.leapdays = -1
213 if yday:
214 ydayidx = [31, 59, 90, 120, 151, 181, 212,
215 243, 273, 304, 334, 366]
216 for idx, ydays in enumerate(ydayidx):
217 if yday <= ydays:
218 self.month = idx+1
219 if idx == 0:
220 self.day = yday
221 else:
222 self.day = yday-ydayidx[idx-1]
223 break
224 else:
225 raise ValueError("invalid year day (%d)" % yday)
226
227 self._fix()
228
229 def _fix(self):
230 if abs(self.microseconds) > 999999:
231 s = _sign(self.microseconds)
232 div, mod = divmod(self.microseconds * s, 1000000)
233 self.microseconds = mod * s
234 self.seconds += div * s
235 if abs(self.seconds) > 59:
236 s = _sign(self.seconds)
237 div, mod = divmod(self.seconds * s, 60)
238 self.seconds = mod * s
239 self.minutes += div * s
240 if abs(self.minutes) > 59:
241 s = _sign(self.minutes)
242 div, mod = divmod(self.minutes * s, 60)
243 self.minutes = mod * s
244 self.hours += div * s
245 if abs(self.hours) > 23:
246 s = _sign(self.hours)
247 div, mod = divmod(self.hours * s, 24)
248 self.hours = mod * s
249 self.days += div * s
250 if abs(self.months) > 11:
251 s = _sign(self.months)
252 div, mod = divmod(self.months * s, 12)
253 self.months = mod * s
254 self.years += div * s
255 if (self.hours or self.minutes or self.seconds or self.microseconds
256 or self.hour is not None or self.minute is not None or
257 self.second is not None or self.microsecond is not None):
258 self._has_time = 1
259 else:
260 self._has_time = 0
261
262 @property
263 def weeks(self):
264 return int(self.days / 7.0)
265
266 @weeks.setter
267 def weeks(self, value):
268 self.days = self.days - (self.weeks * 7) + value * 7
269
270 def _set_months(self, months):
271 self.months = months
272 if abs(self.months) > 11:
273 s = _sign(self.months)
274 div, mod = divmod(self.months * s, 12)
275 self.months = mod * s
276 self.years = div * s
277 else:
278 self.years = 0
279
280 def normalized(self):
281 """
282 Return a version of this object represented entirely using integer
283 values for the relative attributes.
284
285 >>> relativedelta(days=1.5, hours=2).normalized()
286 relativedelta(days=+1, hours=+14)
287
288 :return:
289 Returns a :class:`dateutil.relativedelta.relativedelta` object.
290 """
291 # Cascade remainders down (rounding each to roughly nearest microsecond)
292 days = int(self.days)
293
294 hours_f = round(self.hours + 24 * (self.days - days), 11)
295 hours = int(hours_f)
296
297 minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
298 minutes = int(minutes_f)
299
300 seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
301 seconds = int(seconds_f)
302
303 microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
304
305 # Constructor carries overflow back up with call to _fix()
306 return self.__class__(years=self.years, months=self.months,
307 days=days, hours=hours, minutes=minutes,
308 seconds=seconds, microseconds=microseconds,
309 leapdays=self.leapdays, year=self.year,
310 month=self.month, day=self.day,
311 weekday=self.weekday, hour=self.hour,
312 minute=self.minute, second=self.second,
313 microsecond=self.microsecond)
314
315 def __add__(self, other):
316 if isinstance(other, relativedelta):
317 return self.__class__(years=other.years + self.years,
318 months=other.months + self.months,
319 days=other.days + self.days,
320 hours=other.hours + self.hours,
321 minutes=other.minutes + self.minutes,
322 seconds=other.seconds + self.seconds,
323 microseconds=(other.microseconds +
324 self.microseconds),
325 leapdays=other.leapdays or self.leapdays,
326 year=(other.year if other.year is not None
327 else self.year),
328 month=(other.month if other.month is not None
329 else self.month),
330 day=(other.day if other.day is not None
331 else self.day),
332 weekday=(other.weekday if other.weekday is not None
333 else self.weekday),
334 hour=(other.hour if other.hour is not None
335 else self.hour),
336 minute=(other.minute if other.minute is not None
337 else self.minute),
338 second=(other.second if other.second is not None
339 else self.second),
340 microsecond=(other.microsecond if other.microsecond
341 is not None else
342 self.microsecond))
343 if isinstance(other, datetime.timedelta):
344 return self.__class__(years=self.years,
345 months=self.months,
346 days=self.days + other.days,
347 hours=self.hours,
348 minutes=self.minutes,
349 seconds=self.seconds + other.seconds,
350 microseconds=self.microseconds + other.microseconds,
351 leapdays=self.leapdays,
352 year=self.year,
353 month=self.month,
354 day=self.day,
355 weekday=self.weekday,
356 hour=self.hour,
357 minute=self.minute,
358 second=self.second,
359 microsecond=self.microsecond)
360 if not isinstance(other, datetime.date):
361 return NotImplemented
362 elif self._has_time and not isinstance(other, datetime.datetime):
363 other = datetime.datetime.fromordinal(other.toordinal())
364 year = (self.year or other.year)+self.years
365 month = self.month or other.month
366 if self.months:
367 assert 1 <= abs(self.months) <= 12
368 month += self.months
369 if month > 12:
370 year += 1
371 month -= 12
372 elif month < 1:
373 year -= 1
374 month += 12
375 day = min(calendar.monthrange(year, month)[1],
376 self.day or other.day)
377 repl = {"year": year, "month": month, "day": day}
378 for attr in ["hour", "minute", "second", "microsecond"]:
379 value = getattr(self, attr)
380 if value is not None:
381 repl[attr] = value
382 days = self.days
383 if self.leapdays and month > 2 and calendar.isleap(year):
384 days += self.leapdays
385 ret = (other.replace(**repl)
386 + datetime.timedelta(days=days,
387 hours=self.hours,
388 minutes=self.minutes,
389 seconds=self.seconds,
390 microseconds=self.microseconds))
391 if self.weekday:
392 weekday, nth = self.weekday.weekday, self.weekday.n or 1
393 jumpdays = (abs(nth) - 1) * 7
394 if nth > 0:
395 jumpdays += (7 - ret.weekday() + weekday) % 7
396 else:
397 jumpdays += (ret.weekday() - weekday) % 7
398 jumpdays *= -1
399 ret += datetime.timedelta(days=jumpdays)
400 return ret
401
402 def __radd__(self, other):
403 return self.__add__(other)
404
405 def __rsub__(self, other):
406 return self.__neg__().__radd__(other)
407
408 def __sub__(self, other):
409 if not isinstance(other, relativedelta):
410 return NotImplemented # In case the other object defines __rsub__
411 return self.__class__(years=self.years - other.years,
412 months=self.months - other.months,
413 days=self.days - other.days,
414 hours=self.hours - other.hours,
415 minutes=self.minutes - other.minutes,
416 seconds=self.seconds - other.seconds,
417 microseconds=self.microseconds - other.microseconds,
418 leapdays=self.leapdays or other.leapdays,
419 year=(self.year if self.year is not None
420 else other.year),
421 month=(self.month if self.month is not None else
422 other.month),
423 day=(self.day if self.day is not None else
424 other.day),
425 weekday=(self.weekday if self.weekday is not None else
426 other.weekday),
427 hour=(self.hour if self.hour is not None else
428 other.hour),
429 minute=(self.minute if self.minute is not None else
430 other.minute),
431 second=(self.second if self.second is not None else
432 other.second),
433 microsecond=(self.microsecond if self.microsecond
434 is not None else
435 other.microsecond))
436
437 def __abs__(self):
438 return self.__class__(years=abs(self.years),
439 months=abs(self.months),
440 days=abs(self.days),
441 hours=abs(self.hours),
442 minutes=abs(self.minutes),
443 seconds=abs(self.seconds),
444 microseconds=abs(self.microseconds),
445 leapdays=self.leapdays,
446 year=self.year,
447 month=self.month,
448 day=self.day,
449 weekday=self.weekday,
450 hour=self.hour,
451 minute=self.minute,
452 second=self.second,
453 microsecond=self.microsecond)
454
455 def __neg__(self):
456 return self.__class__(years=-self.years,
457 months=-self.months,
458 days=-self.days,
459 hours=-self.hours,
460 minutes=-self.minutes,
461 seconds=-self.seconds,
462 microseconds=-self.microseconds,
463 leapdays=self.leapdays,
464 year=self.year,
465 month=self.month,
466 day=self.day,
467 weekday=self.weekday,
468 hour=self.hour,
469 minute=self.minute,
470 second=self.second,
471 microsecond=self.microsecond)
472
473 def __bool__(self):
474 return not (not self.years and
475 not self.months and
476 not self.days and
477 not self.hours and
478 not self.minutes and
479 not self.seconds and
480 not self.microseconds and
481 not self.leapdays and
482 self.year is None and
483 self.month is None and
484 self.day is None and
485 self.weekday is None and
486 self.hour is None and
487 self.minute is None and
488 self.second is None and
489 self.microsecond is None)
490
491 def __mul__(self, other):
492 try:
493 f = float(other)
494 except TypeError:
495 return NotImplemented
496
497 return self.__class__(years=int(self.years * f),
498 months=int(self.months * f),
499 days=int(self.days * f),
500 hours=int(self.hours * f),
501 minutes=int(self.minutes * f),
502 seconds=int(self.seconds * f),
503 microseconds=int(self.microseconds * f),
504 leapdays=self.leapdays,
505 year=self.year,
506 month=self.month,
507 day=self.day,
508 weekday=self.weekday,
509 hour=self.hour,
510 minute=self.minute,
511 second=self.second,
512 microsecond=self.microsecond)
513
514 __rmul__ = __mul__
515
516 def __eq__(self, other):
517 if not isinstance(other, relativedelta):
518 return NotImplemented
519 if self.weekday or other.weekday:
520 if not self.weekday or not other.weekday:
521 return False
522 if self.weekday.weekday != other.weekday.weekday:
523 return False
524 n1, n2 = self.weekday.n, other.weekday.n
525 if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
526 return False
527 return (self.years == other.years and
528 self.months == other.months and
529 self.days == other.days and
530 self.hours == other.hours and
531 self.minutes == other.minutes and
532 self.seconds == other.seconds and
533 self.microseconds == other.microseconds and
534 self.leapdays == other.leapdays and
535 self.year == other.year and
536 self.month == other.month and
537 self.day == other.day and
538 self.hour == other.hour and
539 self.minute == other.minute and
540 self.second == other.second and
541 self.microsecond == other.microsecond)
542
543 def __hash__(self):
544 return hash((
545 self.weekday,
546 self.years,
547 self.months,
548 self.days,
549 self.hours,
550 self.minutes,
551 self.seconds,
552 self.microseconds,
553 self.leapdays,
554 self.year,
555 self.month,
556 self.day,
557 self.hour,
558 self.minute,
559 self.second,
560 self.microsecond,
561 ))
562
563 def __truediv__(self, other):
564 try:
565 reciprocal = 1 / float(other)
566 except TypeError:
567 return NotImplemented
568
569 return self.__mul__(reciprocal)
570
571
572 def __repr__(self):
573 l = []
574 for attr in ["years", "months", "days", "leapdays",
575 "hours", "minutes", "seconds", "microseconds"]:
576 value = getattr(self, attr)
577 if value:
578 l.append("{attr}={value:+g}".format(attr=attr, value=value))
579 for attr in ["year", "month", "day", "weekday",
580 "hour", "minute", "second", "microsecond"]:
581 value = getattr(self, attr)
582 if value is not None:
583 l.append("{attr}={value}".format(attr=attr, value=repr(value)))
584 return "{classname}({attrs})".format(classname=self.__class__.__name__,
585 attrs=", ".join(l))
586
587
588def _sign(x):
589 return int(copysign(1, x))
590
591# vim:ts=4:sw=4:et