1
# -*- coding: utf-8 -*-
2 20
import datetime
3 20
import calendar
4

5 20
import operator
6 20
from math import copysign
7

8 20
from six import integer_types
9 20
from warnings import warn
10

11 20
from ._common import weekday
12

13 20
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
14

15 20
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
16

17

18 20
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 20
    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 20
        if dt1 and dt2:
113
            # datetime is a subclass of date. So both must be date
114 20
            if not (isinstance(dt1, datetime.date) and
115
                    isinstance(dt2, datetime.date)):
116 20
                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 20
            if (isinstance(dt1, datetime.datetime) !=
121
                    isinstance(dt2, datetime.datetime)):
122 20
                if not isinstance(dt1, datetime.datetime):
123 20
                    dt1 = datetime.datetime.fromordinal(dt1.toordinal())
124 20
                elif not isinstance(dt2, datetime.datetime):
125 20
                    dt2 = datetime.datetime.fromordinal(dt2.toordinal())
126

127 20
            self.years = 0
128 20
            self.months = 0
129 20
            self.days = 0
130 20
            self.leapdays = 0
131 20
            self.hours = 0
132 20
            self.minutes = 0
133 20
            self.seconds = 0
134 20
            self.microseconds = 0
135 20
            self.year = None
136 20
            self.month = None
137 20
            self.day = None
138 20
            self.weekday = None
139 20
            self.hour = None
140 20
            self.minute = None
141 20
            self.second = None
142 20
            self.microsecond = None
143 20
            self._has_time = 0
144

145
            # Get year / month delta between the two
146 20
            months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
147 20
            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 20
            dtm = self.__radd__(dt2)
152

153
            # If we've overshot our target, make an adjustment
154 20
            if dt1 < dt2:
155 20
                compare = operator.gt
156 20
                increment = 1
157
            else:
158 20
                compare = operator.lt
159 20
                increment = -1
160

161 20
            while compare(dt1, dtm):
162 20
                months += increment
163 20
                self._set_months(months)
164 20
                dtm = self.__radd__(dt2)
165

166
            # Get the timedelta between the "months-adjusted" date and dt1
167 20
            delta = dt1 - dtm
168 20
            self.seconds = delta.seconds + delta.days * 86400
169 20
            self.microseconds = delta.microseconds
170
        else:
171
            # Check for non-integer values in integer-only quantities
172 20
            if any(x is not None and x != int(x) for x in (years, months)):
173 20
                raise ValueError("Non-integer years and months are "
174
                                 "ambiguous and not currently supported.")
175

176
            # Relative information
177 20
            self.years = int(years)
178 20
            self.months = int(months)
179 20
            self.days = days + weeks * 7
180 20
            self.leapdays = leapdays
181 20
            self.hours = hours
182 20
            self.minutes = minutes
183 20
            self.seconds = seconds
184 20
            self.microseconds = microseconds
185

186
            # Absolute information
187 20
            self.year = year
188 20
            self.month = month
189 20
            self.day = day
190 20
            self.hour = hour
191 20
            self.minute = minute
192 20
            self.second = second
193 20
            self.microsecond = microsecond
194

195 20
            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 20
                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 20
            if isinstance(weekday, integer_types):
204 20
                self.weekday = weekdays[weekday]
205
            else:
206 20
                self.weekday = weekday
207

208 20
            yday = 0
209 20
            if nlyearday:
210 20
                yday = nlyearday
211 20
            elif yearday:
212 20
                yday = yearday
213 20
                if yearday > 59:
214 20
                    self.leapdays = -1
215 20
            if yday:
216 20
                ydayidx = [31, 59, 90, 120, 151, 181, 212,
217
                           243, 273, 304, 334, 366]
218 20
                for idx, ydays in enumerate(ydayidx):
219 20
                    if yday <= ydays:
220 20
                        self.month = idx+1
221 20
                        if idx == 0:
222 20
                            self.day = yday
223
                        else:
224 20
                            self.day = yday-ydayidx[idx-1]
225 20
                        break
226
                else:
227 20
                    raise ValueError("invalid year day (%d)" % yday)
228

229 20
        self._fix()
230

231 20
    def _fix(self):
232 20
        if abs(self.microseconds) > 999999:
233 20
            s = _sign(self.microseconds)
234 20
            div, mod = divmod(self.microseconds * s, 1000000)
235 20
            self.microseconds = mod * s
236 20
            self.seconds += div * s
237 20
        if abs(self.seconds) > 59:
238 20
            s = _sign(self.seconds)
239 20
            div, mod = divmod(self.seconds * s, 60)
240 20
            self.seconds = mod * s
241 20
            self.minutes += div * s
242 20
        if abs(self.minutes) > 59:
243 20
            s = _sign(self.minutes)
244 20
            div, mod = divmod(self.minutes * s, 60)
245 20
            self.minutes = mod * s
246 20
            self.hours += div * s
247 20
        if abs(self.hours) > 23:
248 20
            s = _sign(self.hours)
249 20
            div, mod = divmod(self.hours * s, 24)
250 20
            self.hours = mod * s
251 20
            self.days += div * s
252 20
        if abs(self.months) > 11:
253 20
            s = _sign(self.months)
254 20
            div, mod = divmod(self.months * s, 12)
255 20
            self.months = mod * s
256 20
            self.years += div * s
257 20
        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 20
            self._has_time = 1
261
        else:
262 20
            self._has_time = 0
263

264 20
    @property
265 2
    def weeks(self):
266 20
        return int(self.days / 7.0)
267

268 20
    @weeks.setter
269 2
    def weeks(self, value):
270 20
        self.days = self.days - (self.weeks * 7) + value * 7
271

272 20
    def _set_months(self, months):
273 20
        self.months = months
274 20
        if abs(self.months) > 11:
275 20
            s = _sign(self.months)
276 20
            div, mod = divmod(self.months * s, 12)
277 20
            self.months = mod * s
278 20
            self.years = div * s
279
        else:
280 20
            self.years = 0
281

282 20
    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 20
        days = int(self.days)
295

296 20
        hours_f = round(self.hours + 24 * (self.days - days), 11)
297 20
        hours = int(hours_f)
298

299 20
        minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
300 20
        minutes = int(minutes_f)
301

302 20
        seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
303 20
        seconds = int(seconds_f)
304

305 20
        microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
306

307
        # Constructor carries overflow back up with call to _fix()
308 20
        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 20
    def __add__(self, other):
318 20
        if isinstance(other, relativedelta):
319 20
            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 20
        if isinstance(other, datetime.timedelta):
346 20
            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 20
        if not isinstance(other, datetime.date):
363 20
            return NotImplemented
364 20
        elif self._has_time and not isinstance(other, datetime.datetime):
365 20
            other = datetime.datetime.fromordinal(other.toordinal())
366 20
        year = (self.year or other.year)+self.years
367 20
        month = self.month or other.month
368 20
        if self.months:
369 20
            assert 1 <= abs(self.months) <= 12
370 20
            month += self.months
371 20
            if month > 12:
372 20
                year += 1
373 20
                month -= 12
374 20
            elif month < 1:
375 20
                year -= 1
376 20
                month += 12
377 20
        day = min(calendar.monthrange(year, month)[1],
378
                  self.day or other.day)
379 20
        repl = {"year": year, "month": month, "day": day}
380 20
        for attr in ["hour", "minute", "second", "microsecond"]:
381 20
            value = getattr(self, attr)
382 20
            if value is not None:
383 20
                repl[attr] = value
384 20
        days = self.days
385 20
        if self.leapdays and month > 2 and calendar.isleap(year):
386 20
            days += self.leapdays
387 20
        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 20
        if self.weekday:
394 20
            weekday, nth = self.weekday.weekday, self.weekday.n or 1
395 20
            jumpdays = (abs(nth) - 1) * 7
396 20
            if nth > 0:
397 20
                jumpdays += (7 - ret.weekday() + weekday) % 7
398
            else:
399 20
                jumpdays += (ret.weekday() - weekday) % 7
400 20
                jumpdays *= -1
401 20
            ret += datetime.timedelta(days=jumpdays)
402 20
        return ret
403

404 20
    def __radd__(self, other):
405 20
        return self.__add__(other)
406

407 20
    def __rsub__(self, other):
408 20
        return self.__neg__().__radd__(other)
409

410 20
    def __sub__(self, other):
411 20
        if not isinstance(other, relativedelta):
412 20
            return NotImplemented   # In case the other object defines __rsub__
413 20
        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 20
    def __abs__(self):
440 20
        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 20
    def __neg__(self):
458 20
        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 20
    def __bool__(self):
476 20
        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 20
    __nonzero__ = __bool__
494

495 20
    def __mul__(self, other):
496 20
        try:
497 20
            f = float(other)
498 20
        except TypeError:
499 20
            return NotImplemented
500

501 20
        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 20
    __rmul__ = __mul__
519

520 20
    def __eq__(self, other):
521 20
        if not isinstance(other, relativedelta):
522 20
            return NotImplemented
523 20
        if self.weekday or other.weekday:
524 20
            if not self.weekday or not other.weekday:
525 20
                return False
526 20
            if self.weekday.weekday != other.weekday.weekday:
527 20
                return False
528 20
            n1, n2 = self.weekday.n, other.weekday.n
529 20
            if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
530 20
                return False
531 20
        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 20
    def __hash__(self):
548 20
        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 20
    def __ne__(self, other):
568 20
        return not self.__eq__(other)
569

570 20
    def __div__(self, other):
571 20
        try:
572 20
            reciprocal = 1 / float(other)
573 20
        except TypeError:
574 20
            return NotImplemented
575

576 20
        return self.__mul__(reciprocal)
577

578 20
    __truediv__ = __div__
579

580 20
    def __repr__(self):
581 20
        l = []
582 20
        for attr in ["years", "months", "days", "leapdays",
583
                     "hours", "minutes", "seconds", "microseconds"]:
584 20
            value = getattr(self, attr)
585 20
            if value:
586 20
                l.append("{attr}={value:+g}".format(attr=attr, value=value))
587 20
        for attr in ["year", "month", "day", "weekday",
588
                     "hour", "minute", "second", "microsecond"]:
589 20
            value = getattr(self, attr)
590 20
            if value is not None:
591 20
                l.append("{attr}={value}".format(attr=attr, value=repr(value)))
592 20
        return "{classname}({attrs})".format(classname=self.__class__.__name__,
593
                                             attrs=", ".join(l))
594

595

596 20
def _sign(x):
597 20
    return int(copysign(1, x))
598

599
# vim:ts=4:sw=4:et

Read our documentation on viewing source code .

Loading