123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600 |
- # -*- coding: utf-8 -*-
- import datetime
- import calendar
- import operator
- from math import copysign
- from six import integer_types
- from warnings import warn
- from ._common import weekday
- MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
- __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
- class relativedelta(object):
- """
- The relativedelta type is designed to be applied to an existing datetime and
- can replace specific components of that datetime, or represents an interval
- of time.
- It is based on the specification of the excellent work done by M.-A. Lemburg
- in his
- `mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
- However, notice that this type does *NOT* implement the same algorithm as
- his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
- There are two different ways to build a relativedelta instance. The
- first one is passing it two date/datetime classes::
- relativedelta(datetime1, datetime2)
- The second one is passing it any number of the following keyword arguments::
- relativedelta(arg1=x,arg2=y,arg3=z...)
- year, month, day, hour, minute, second, microsecond:
- Absolute information (argument is singular); adding or subtracting a
- relativedelta with absolute information does not perform an arithmetic
- operation, but rather REPLACES the corresponding value in the
- original datetime with the value(s) in relativedelta.
- years, months, weeks, days, hours, minutes, seconds, microseconds:
- Relative information, may be negative (argument is plural); adding
- or subtracting a relativedelta with relative information performs
- the corresponding aritmetic operation on the original datetime value
- with the information in the relativedelta.
- weekday:
- One of the weekday instances (MO, TU, etc) available in the
- relativedelta module. These instances may receive a parameter N,
- specifying the Nth weekday, which could be positive or negative
- (like MO(+1) or MO(-2)). Not specifying it is the same as specifying
- +1. You can also use an integer, where 0=MO. This argument is always
- relative e.g. if the calculated date is already Monday, using MO(1)
- or MO(-1) won't change the day. To effectively make it absolute, use
- it in combination with the day argument (e.g. day=1, MO(1) for first
- Monday of the month).
- leapdays:
- Will add given days to the date found, if year is a leap
- year, and the date found is post 28 of february.
- yearday, nlyearday:
- Set the yearday or the non-leap year day (jump leap days).
- These are converted to day/month/leapdays information.
- There are relative and absolute forms of the keyword
- arguments. The plural is relative, and the singular is
- absolute. For each argument in the order below, the absolute form
- is applied first (by setting each attribute to that value) and
- then the relative form (by adding the value to the attribute).
- The order of attributes considered when this relativedelta is
- added to a datetime is:
- 1. Year
- 2. Month
- 3. Day
- 4. Hours
- 5. Minutes
- 6. Seconds
- 7. Microseconds
- Finally, weekday is applied, using the rule described above.
- For example
- >>> from datetime import datetime
- >>> from dateutil.relativedelta import relativedelta, MO
- >>> dt = datetime(2018, 4, 9, 13, 37, 0)
- >>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
- >>> dt + delta
- datetime.datetime(2018, 4, 2, 14, 37)
- First, the day is set to 1 (the first of the month), then 25 hours
- are added, to get to the 2nd day and 14th hour, finally the
- weekday is applied, but since the 2nd is already a Monday there is
- no effect.
- """
- def __init__(self, dt1=None, dt2=None,
- years=0, months=0, days=0, leapdays=0, weeks=0,
- hours=0, minutes=0, seconds=0, microseconds=0,
- year=None, month=None, day=None, weekday=None,
- yearday=None, nlyearday=None,
- hour=None, minute=None, second=None, microsecond=None):
- if dt1 and dt2:
- # datetime is a subclass of date. So both must be date
- if not (isinstance(dt1, datetime.date) and
- isinstance(dt2, datetime.date)):
- raise TypeError("relativedelta only diffs datetime/date")
- # We allow two dates, or two datetimes, so we coerce them to be
- # of the same type
- if (isinstance(dt1, datetime.datetime) !=
- isinstance(dt2, datetime.datetime)):
- if not isinstance(dt1, datetime.datetime):
- dt1 = datetime.datetime.fromordinal(dt1.toordinal())
- elif not isinstance(dt2, datetime.datetime):
- dt2 = datetime.datetime.fromordinal(dt2.toordinal())
- self.years = 0
- self.months = 0
- self.days = 0
- self.leapdays = 0
- self.hours = 0
- self.minutes = 0
- self.seconds = 0
- self.microseconds = 0
- self.year = None
- self.month = None
- self.day = None
- self.weekday = None
- self.hour = None
- self.minute = None
- self.second = None
- self.microsecond = None
- self._has_time = 0
- # Get year / month delta between the two
- months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
- self._set_months(months)
- # Remove the year/month delta so the timedelta is just well-defined
- # time units (seconds, days and microseconds)
- dtm = self.__radd__(dt2)
- # If we've overshot our target, make an adjustment
- if dt1 < dt2:
- compare = operator.gt
- increment = 1
- else:
- compare = operator.lt
- increment = -1
- while compare(dt1, dtm):
- months += increment
- self._set_months(months)
- dtm = self.__radd__(dt2)
- # Get the timedelta between the "months-adjusted" date and dt1
- delta = dt1 - dtm
- self.seconds = delta.seconds + delta.days * 86400
- self.microseconds = delta.microseconds
- else:
- # Check for non-integer values in integer-only quantities
- if any(x is not None and x != int(x) for x in (years, months)):
- raise ValueError("Non-integer years and months are "
- "ambiguous and not currently supported.")
- # Relative information
- self.years = int(years)
- self.months = int(months)
- self.days = days + weeks * 7
- self.leapdays = leapdays
- self.hours = hours
- self.minutes = minutes
- self.seconds = seconds
- self.microseconds = microseconds
- # Absolute information
- self.year = year
- self.month = month
- self.day = day
- self.hour = hour
- self.minute = minute
- self.second = second
- self.microsecond = microsecond
- if any(x is not None and int(x) != x
- for x in (year, month, day, hour,
- minute, second, microsecond)):
- # For now we'll deprecate floats - later it'll be an error.
- warn("Non-integer value passed as absolute information. " +
- "This is not a well-defined condition and will raise " +
- "errors in future versions.", DeprecationWarning)
- if isinstance(weekday, integer_types):
- self.weekday = weekdays[weekday]
- else:
- self.weekday = weekday
- yday = 0
- if nlyearday:
- yday = nlyearday
- elif yearday:
- yday = yearday
- if yearday > 59:
- self.leapdays = -1
- if yday:
- ydayidx = [31, 59, 90, 120, 151, 181, 212,
- 243, 273, 304, 334, 366]
- for idx, ydays in enumerate(ydayidx):
- if yday <= ydays:
- self.month = idx+1
- if idx == 0:
- self.day = yday
- else:
- self.day = yday-ydayidx[idx-1]
- break
- else:
- raise ValueError("invalid year day (%d)" % yday)
- self._fix()
- def _fix(self):
- if abs(self.microseconds) > 999999:
- s = _sign(self.microseconds)
- div, mod = divmod(self.microseconds * s, 1000000)
- self.microseconds = mod * s
- self.seconds += div * s
- if abs(self.seconds) > 59:
- s = _sign(self.seconds)
- div, mod = divmod(self.seconds * s, 60)
- self.seconds = mod * s
- self.minutes += div * s
- if abs(self.minutes) > 59:
- s = _sign(self.minutes)
- div, mod = divmod(self.minutes * s, 60)
- self.minutes = mod * s
- self.hours += div * s
- if abs(self.hours) > 23:
- s = _sign(self.hours)
- div, mod = divmod(self.hours * s, 24)
- self.hours = mod * s
- self.days += div * s
- if abs(self.months) > 11:
- s = _sign(self.months)
- div, mod = divmod(self.months * s, 12)
- self.months = mod * s
- self.years += div * s
- if (self.hours or self.minutes or self.seconds or self.microseconds
- or self.hour is not None or self.minute is not None or
- self.second is not None or self.microsecond is not None):
- self._has_time = 1
- else:
- self._has_time = 0
- @property
- def weeks(self):
- return int(self.days / 7.0)
- @weeks.setter
- def weeks(self, value):
- self.days = self.days - (self.weeks * 7) + value * 7
- def _set_months(self, months):
- self.months = months
- if abs(self.months) > 11:
- s = _sign(self.months)
- div, mod = divmod(self.months * s, 12)
- self.months = mod * s
- self.years = div * s
- else:
- self.years = 0
- def normalized(self):
- """
- Return a version of this object represented entirely using integer
- values for the relative attributes.
- >>> relativedelta(days=1.5, hours=2).normalized()
- relativedelta(days=+1, hours=+14)
- :return:
- Returns a :class:`dateutil.relativedelta.relativedelta` object.
- """
- # Cascade remainders down (rounding each to roughly nearest microsecond)
- days = int(self.days)
- hours_f = round(self.hours + 24 * (self.days - days), 11)
- hours = int(hours_f)
- minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
- minutes = int(minutes_f)
- seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
- seconds = int(seconds_f)
- microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
- # Constructor carries overflow back up with call to _fix()
- return self.__class__(years=self.years, months=self.months,
- days=days, hours=hours, minutes=minutes,
- seconds=seconds, microseconds=microseconds,
- leapdays=self.leapdays, year=self.year,
- month=self.month, day=self.day,
- weekday=self.weekday, hour=self.hour,
- minute=self.minute, second=self.second,
- microsecond=self.microsecond)
- def __add__(self, other):
- if isinstance(other, relativedelta):
- return self.__class__(years=other.years + self.years,
- months=other.months + self.months,
- days=other.days + self.days,
- hours=other.hours + self.hours,
- minutes=other.minutes + self.minutes,
- seconds=other.seconds + self.seconds,
- microseconds=(other.microseconds +
- self.microseconds),
- leapdays=other.leapdays or self.leapdays,
- year=(other.year if other.year is not None
- else self.year),
- month=(other.month if other.month is not None
- else self.month),
- day=(other.day if other.day is not None
- else self.day),
- weekday=(other.weekday if other.weekday is not None
- else self.weekday),
- hour=(other.hour if other.hour is not None
- else self.hour),
- minute=(other.minute if other.minute is not None
- else self.minute),
- second=(other.second if other.second is not None
- else self.second),
- microsecond=(other.microsecond if other.microsecond
- is not None else
- self.microsecond))
- if isinstance(other, datetime.timedelta):
- return self.__class__(years=self.years,
- months=self.months,
- days=self.days + other.days,
- hours=self.hours,
- minutes=self.minutes,
- seconds=self.seconds + other.seconds,
- microseconds=self.microseconds + other.microseconds,
- leapdays=self.leapdays,
- year=self.year,
- month=self.month,
- day=self.day,
- weekday=self.weekday,
- hour=self.hour,
- minute=self.minute,
- second=self.second,
- microsecond=self.microsecond)
- if not isinstance(other, datetime.date):
- return NotImplemented
- elif self._has_time and not isinstance(other, datetime.datetime):
- other = datetime.datetime.fromordinal(other.toordinal())
- year = (self.year or other.year)+self.years
- month = self.month or other.month
- if self.months:
- assert 1 <= abs(self.months) <= 12
- month += self.months
- if month > 12:
- year += 1
- month -= 12
- elif month < 1:
- year -= 1
- month += 12
- day = min(calendar.monthrange(year, month)[1],
- self.day or other.day)
- repl = {"year": year, "month": month, "day": day}
- for attr in ["hour", "minute", "second", "microsecond"]:
- value = getattr(self, attr)
- if value is not None:
- repl[attr] = value
- days = self.days
- if self.leapdays and month > 2 and calendar.isleap(year):
- days += self.leapdays
- ret = (other.replace(**repl)
- + datetime.timedelta(days=days,
- hours=self.hours,
- minutes=self.minutes,
- seconds=self.seconds,
- microseconds=self.microseconds))
- if self.weekday:
- weekday, nth = self.weekday.weekday, self.weekday.n or 1
- jumpdays = (abs(nth) - 1) * 7
- if nth > 0:
- jumpdays += (7 - ret.weekday() + weekday) % 7
- else:
- jumpdays += (ret.weekday() - weekday) % 7
- jumpdays *= -1
- ret += datetime.timedelta(days=jumpdays)
- return ret
- def __radd__(self, other):
- return self.__add__(other)
- def __rsub__(self, other):
- return self.__neg__().__radd__(other)
- def __sub__(self, other):
- if not isinstance(other, relativedelta):
- return NotImplemented # In case the other object defines __rsub__
- return self.__class__(years=self.years - other.years,
- months=self.months - other.months,
- days=self.days - other.days,
- hours=self.hours - other.hours,
- minutes=self.minutes - other.minutes,
- seconds=self.seconds - other.seconds,
- microseconds=self.microseconds - other.microseconds,
- leapdays=self.leapdays or other.leapdays,
- year=(self.year if self.year is not None
- else other.year),
- month=(self.month if self.month is not None else
- other.month),
- day=(self.day if self.day is not None else
- other.day),
- weekday=(self.weekday if self.weekday is not None else
- other.weekday),
- hour=(self.hour if self.hour is not None else
- other.hour),
- minute=(self.minute if self.minute is not None else
- other.minute),
- second=(self.second if self.second is not None else
- other.second),
- microsecond=(self.microsecond if self.microsecond
- is not None else
- other.microsecond))
- def __abs__(self):
- return self.__class__(years=abs(self.years),
- months=abs(self.months),
- days=abs(self.days),
- hours=abs(self.hours),
- minutes=abs(self.minutes),
- seconds=abs(self.seconds),
- microseconds=abs(self.microseconds),
- leapdays=self.leapdays,
- year=self.year,
- month=self.month,
- day=self.day,
- weekday=self.weekday,
- hour=self.hour,
- minute=self.minute,
- second=self.second,
- microsecond=self.microsecond)
- def __neg__(self):
- return self.__class__(years=-self.years,
- months=-self.months,
- days=-self.days,
- hours=-self.hours,
- minutes=-self.minutes,
- seconds=-self.seconds,
- microseconds=-self.microseconds,
- leapdays=self.leapdays,
- year=self.year,
- month=self.month,
- day=self.day,
- weekday=self.weekday,
- hour=self.hour,
- minute=self.minute,
- second=self.second,
- microsecond=self.microsecond)
- def __bool__(self):
- return not (not self.years and
- not self.months and
- not self.days and
- not self.hours and
- not self.minutes and
- not self.seconds and
- not self.microseconds and
- not self.leapdays and
- self.year is None and
- self.month is None and
- self.day is None and
- self.weekday is None and
- self.hour is None and
- self.minute is None and
- self.second is None and
- self.microsecond is None)
- # Compatibility with Python 2.x
- __nonzero__ = __bool__
- def __mul__(self, other):
- try:
- f = float(other)
- except TypeError:
- return NotImplemented
- return self.__class__(years=int(self.years * f),
- months=int(self.months * f),
- days=int(self.days * f),
- hours=int(self.hours * f),
- minutes=int(self.minutes * f),
- seconds=int(self.seconds * f),
- microseconds=int(self.microseconds * f),
- leapdays=self.leapdays,
- year=self.year,
- month=self.month,
- day=self.day,
- weekday=self.weekday,
- hour=self.hour,
- minute=self.minute,
- second=self.second,
- microsecond=self.microsecond)
- __rmul__ = __mul__
- def __eq__(self, other):
- if not isinstance(other, relativedelta):
- return NotImplemented
- if self.weekday or other.weekday:
- if not self.weekday or not other.weekday:
- return False
- if self.weekday.weekday != other.weekday.weekday:
- return False
- n1, n2 = self.weekday.n, other.weekday.n
- if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
- return False
- return (self.years == other.years and
- self.months == other.months and
- self.days == other.days and
- self.hours == other.hours and
- self.minutes == other.minutes and
- self.seconds == other.seconds and
- self.microseconds == other.microseconds and
- self.leapdays == other.leapdays and
- self.year == other.year and
- self.month == other.month and
- self.day == other.day and
- self.hour == other.hour and
- self.minute == other.minute and
- self.second == other.second and
- self.microsecond == other.microsecond)
- def __hash__(self):
- return hash((
- self.weekday,
- self.years,
- self.months,
- self.days,
- self.hours,
- self.minutes,
- self.seconds,
- self.microseconds,
- self.leapdays,
- self.year,
- self.month,
- self.day,
- self.hour,
- self.minute,
- self.second,
- self.microsecond,
- ))
- def __ne__(self, other):
- return not self.__eq__(other)
- def __div__(self, other):
- try:
- reciprocal = 1 / float(other)
- except TypeError:
- return NotImplemented
- return self.__mul__(reciprocal)
- __truediv__ = __div__
- def __repr__(self):
- l = []
- for attr in ["years", "months", "days", "leapdays",
- "hours", "minutes", "seconds", "microseconds"]:
- value = getattr(self, attr)
- if value:
- l.append("{attr}={value:+g}".format(attr=attr, value=value))
- for attr in ["year", "month", "day", "weekday",
- "hour", "minute", "second", "microsecond"]:
- value = getattr(self, attr)
- if value is not None:
- l.append("{attr}={value}".format(attr=attr, value=repr(value)))
- return "{classname}({attrs})".format(classname=self.__class__.__name__,
- attrs=", ".join(l))
- def _sign(x):
- return int(copysign(1, x))
- # vim:ts=4:sw=4:et
|