relativedelta.py 24 KB

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