You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

590 lines
24 KiB

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