relativedelta.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  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 <http://www.egenix.com/files/python/mxDateTime.html>`_ 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 aritmetic
  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 instances may
  35. receive a parameter N, specifying the Nth weekday, which could
  36. be positive or negative (like MO(+1) or MO(-2). Not specifying
  37. it is the same as specifying +1. You can also use an integer,
  38. where 0=MO.
  39. leapdays:
  40. Will add given days to the date found, if year is a leap
  41. year, and the date found is post 28 of february.
  42. yearday, nlyearday:
  43. Set the yearday or the non-leap year day (jump leap days).
  44. These are converted to day/month/leapdays information.
  45. Here is the behavior of operations with relativedelta:
  46. 1. Calculate the absolute year, using the 'year' argument, or the
  47. original datetime year, if the argument is not present.
  48. 2. Add the relative 'years' argument to the absolute year.
  49. 3. Do steps 1 and 2 for month/months.
  50. 4. Calculate the absolute day, using the 'day' argument, or the
  51. original datetime day, if the argument is not present. Then,
  52. subtract from the day until it fits in the year and month
  53. found after their operations.
  54. 5. Add the relative 'days' argument to the absolute day. Notice
  55. that the 'weeks' argument is multiplied by 7 and added to
  56. 'days'.
  57. 6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
  58. microsecond/microseconds.
  59. 7. If the 'weekday' argument is present, calculate the weekday,
  60. with the given (wday, nth) tuple. wday is the index of the
  61. weekday (0-6, 0=Mon), and nth is the number of weeks to add
  62. forward or backward, depending on its signal. Notice that if
  63. the calculated date is already Monday, for example, using
  64. (0, 1) or (0, -1) won't change the day.
  65. """
  66. def __init__(self, dt1=None, dt2=None,
  67. years=0, months=0, days=0, leapdays=0, weeks=0,
  68. hours=0, minutes=0, seconds=0, microseconds=0,
  69. year=None, month=None, day=None, weekday=None,
  70. yearday=None, nlyearday=None,
  71. hour=None, minute=None, second=None, microsecond=None):
  72. # Check for non-integer values in integer-only quantities
  73. if any(x is not None and x != int(x) for x in (years, months)):
  74. raise ValueError("Non-integer years and months are "
  75. "ambiguous and not currently supported.")
  76. if dt1 and dt2:
  77. # datetime is a subclass of date. So both must be date
  78. if not (isinstance(dt1, datetime.date) and
  79. isinstance(dt2, datetime.date)):
  80. raise TypeError("relativedelta only diffs datetime/date")
  81. # We allow two dates, or two datetimes, so we coerce them to be
  82. # of the same type
  83. if (isinstance(dt1, datetime.datetime) !=
  84. isinstance(dt2, datetime.datetime)):
  85. if not isinstance(dt1, datetime.datetime):
  86. dt1 = datetime.datetime.fromordinal(dt1.toordinal())
  87. elif not isinstance(dt2, datetime.datetime):
  88. dt2 = datetime.datetime.fromordinal(dt2.toordinal())
  89. self.years = 0
  90. self.months = 0
  91. self.days = 0
  92. self.leapdays = 0
  93. self.hours = 0
  94. self.minutes = 0
  95. self.seconds = 0
  96. self.microseconds = 0
  97. self.year = None
  98. self.month = None
  99. self.day = None
  100. self.weekday = None
  101. self.hour = None
  102. self.minute = None
  103. self.second = None
  104. self.microsecond = None
  105. self._has_time = 0
  106. # Get year / month delta between the two
  107. months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
  108. self._set_months(months)
  109. # Remove the year/month delta so the timedelta is just well-defined
  110. # time units (seconds, days and microseconds)
  111. dtm = self.__radd__(dt2)
  112. # If we've overshot our target, make an adjustment
  113. if dt1 < dt2:
  114. compare = operator.gt
  115. increment = 1
  116. else:
  117. compare = operator.lt
  118. increment = -1
  119. while compare(dt1, dtm):
  120. months += increment
  121. self._set_months(months)
  122. dtm = self.__radd__(dt2)
  123. # Get the timedelta between the "months-adjusted" date and dt1
  124. delta = dt1 - dtm
  125. self.seconds = delta.seconds + delta.days * 86400
  126. self.microseconds = delta.microseconds
  127. else:
  128. # Relative information
  129. self.years = years
  130. self.months = months
  131. self.days = days + weeks * 7
  132. self.leapdays = leapdays
  133. self.hours = hours
  134. self.minutes = minutes
  135. self.seconds = seconds
  136. self.microseconds = microseconds
  137. # Absolute information
  138. self.year = year
  139. self.month = month
  140. self.day = day
  141. self.hour = hour
  142. self.minute = minute
  143. self.second = second
  144. self.microsecond = microsecond
  145. if any(x is not None and int(x) != x
  146. for x in (year, month, day, hour,
  147. minute, second, microsecond)):
  148. # For now we'll deprecate floats - later it'll be an error.
  149. warn("Non-integer value passed as absolute information. " +
  150. "This is not a well-defined condition and will raise " +
  151. "errors in future versions.", DeprecationWarning)
  152. if isinstance(weekday, integer_types):
  153. self.weekday = weekdays[weekday]
  154. else:
  155. self.weekday = weekday
  156. yday = 0
  157. if nlyearday:
  158. yday = nlyearday
  159. elif yearday:
  160. yday = yearday
  161. if yearday > 59:
  162. self.leapdays = -1
  163. if yday:
  164. ydayidx = [31, 59, 90, 120, 151, 181, 212,
  165. 243, 273, 304, 334, 366]
  166. for idx, ydays in enumerate(ydayidx):
  167. if yday <= ydays:
  168. self.month = idx+1
  169. if idx == 0:
  170. self.day = yday
  171. else:
  172. self.day = yday-ydayidx[idx-1]
  173. break
  174. else:
  175. raise ValueError("invalid year day (%d)" % yday)
  176. self._fix()
  177. def _fix(self):
  178. if abs(self.microseconds) > 999999:
  179. s = _sign(self.microseconds)
  180. div, mod = divmod(self.microseconds * s, 1000000)
  181. self.microseconds = mod * s
  182. self.seconds += div * s
  183. if abs(self.seconds) > 59:
  184. s = _sign(self.seconds)
  185. div, mod = divmod(self.seconds * s, 60)
  186. self.seconds = mod * s
  187. self.minutes += div * s
  188. if abs(self.minutes) > 59:
  189. s = _sign(self.minutes)
  190. div, mod = divmod(self.minutes * s, 60)
  191. self.minutes = mod * s
  192. self.hours += div * s
  193. if abs(self.hours) > 23:
  194. s = _sign(self.hours)
  195. div, mod = divmod(self.hours * s, 24)
  196. self.hours = mod * s
  197. self.days += div * s
  198. if abs(self.months) > 11:
  199. s = _sign(self.months)
  200. div, mod = divmod(self.months * s, 12)
  201. self.months = mod * s
  202. self.years += div * s
  203. if (self.hours or self.minutes or self.seconds or self.microseconds
  204. or self.hour is not None or self.minute is not None or
  205. self.second is not None or self.microsecond is not None):
  206. self._has_time = 1
  207. else:
  208. self._has_time = 0
  209. @property
  210. def weeks(self):
  211. return self.days // 7
  212. @weeks.setter
  213. def weeks(self, value):
  214. self.days = self.days - (self.weeks * 7) + value * 7
  215. def _set_months(self, months):
  216. self.months = months
  217. if abs(self.months) > 11:
  218. s = _sign(self.months)
  219. div, mod = divmod(self.months * s, 12)
  220. self.months = mod * s
  221. self.years = div * s
  222. else:
  223. self.years = 0
  224. def normalized(self):
  225. """
  226. Return a version of this object represented entirely using integer
  227. values for the relative attributes.
  228. >>> relativedelta(days=1.5, hours=2).normalized()
  229. relativedelta(days=1, hours=14)
  230. :return:
  231. Returns a :class:`dateutil.relativedelta.relativedelta` object.
  232. """
  233. # Cascade remainders down (rounding each to roughly nearest microsecond)
  234. days = int(self.days)
  235. hours_f = round(self.hours + 24 * (self.days - days), 11)
  236. hours = int(hours_f)
  237. minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
  238. minutes = int(minutes_f)
  239. seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
  240. seconds = int(seconds_f)
  241. microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
  242. # Constructor carries overflow back up with call to _fix()
  243. return self.__class__(years=self.years, months=self.months,
  244. days=days, hours=hours, minutes=minutes,
  245. seconds=seconds, microseconds=microseconds,
  246. leapdays=self.leapdays, year=self.year,
  247. month=self.month, day=self.day,
  248. weekday=self.weekday, hour=self.hour,
  249. minute=self.minute, second=self.second,
  250. microsecond=self.microsecond)
  251. def __add__(self, other):
  252. if isinstance(other, relativedelta):
  253. return self.__class__(years=other.years + self.years,
  254. months=other.months + self.months,
  255. days=other.days + self.days,
  256. hours=other.hours + self.hours,
  257. minutes=other.minutes + self.minutes,
  258. seconds=other.seconds + self.seconds,
  259. microseconds=(other.microseconds +
  260. self.microseconds),
  261. leapdays=other.leapdays or self.leapdays,
  262. year=other.year or self.year,
  263. month=other.month or self.month,
  264. day=other.day or self.day,
  265. weekday=other.weekday or self.weekday,
  266. hour=other.hour or self.hour,
  267. minute=other.minute or self.minute,
  268. second=other.second or self.second,
  269. microsecond=(other.microsecond or
  270. self.microsecond))
  271. if isinstance(other, datetime.timedelta):
  272. return self.__class__(years=self.years,
  273. months=self.months,
  274. days=self.days + other.days,
  275. hours=self.hours,
  276. minutes=self.minutes,
  277. seconds=self.seconds + other.seconds,
  278. microseconds=self.microseconds + other.microseconds,
  279. leapdays=self.leapdays,
  280. year=self.year,
  281. month=self.month,
  282. day=self.day,
  283. weekday=self.weekday,
  284. hour=self.hour,
  285. minute=self.minute,
  286. second=self.second,
  287. microsecond=self.microsecond)
  288. if not isinstance(other, datetime.date):
  289. return NotImplemented
  290. elif self._has_time and not isinstance(other, datetime.datetime):
  291. other = datetime.datetime.fromordinal(other.toordinal())
  292. year = (self.year or other.year)+self.years
  293. month = self.month or other.month
  294. if self.months:
  295. assert 1 <= abs(self.months) <= 12
  296. month += self.months
  297. if month > 12:
  298. year += 1
  299. month -= 12
  300. elif month < 1:
  301. year -= 1
  302. month += 12
  303. day = min(calendar.monthrange(year, month)[1],
  304. self.day or other.day)
  305. repl = {"year": year, "month": month, "day": day}
  306. for attr in ["hour", "minute", "second", "microsecond"]:
  307. value = getattr(self, attr)
  308. if value is not None:
  309. repl[attr] = value
  310. days = self.days
  311. if self.leapdays and month > 2 and calendar.isleap(year):
  312. days += self.leapdays
  313. ret = (other.replace(**repl)
  314. + datetime.timedelta(days=days,
  315. hours=self.hours,
  316. minutes=self.minutes,
  317. seconds=self.seconds,
  318. microseconds=self.microseconds))
  319. if self.weekday:
  320. weekday, nth = self.weekday.weekday, self.weekday.n or 1
  321. jumpdays = (abs(nth) - 1) * 7
  322. if nth > 0:
  323. jumpdays += (7 - ret.weekday() + weekday) % 7
  324. else:
  325. jumpdays += (ret.weekday() - weekday) % 7
  326. jumpdays *= -1
  327. ret += datetime.timedelta(days=jumpdays)
  328. return ret
  329. def __radd__(self, other):
  330. return self.__add__(other)
  331. def __rsub__(self, other):
  332. return self.__neg__().__radd__(other)
  333. def __sub__(self, other):
  334. if not isinstance(other, relativedelta):
  335. return NotImplemented # In case the other object defines __rsub__
  336. return self.__class__(years=self.years - other.years,
  337. months=self.months - other.months,
  338. days=self.days - other.days,
  339. hours=self.hours - other.hours,
  340. minutes=self.minutes - other.minutes,
  341. seconds=self.seconds - other.seconds,
  342. microseconds=self.microseconds - other.microseconds,
  343. leapdays=self.leapdays or other.leapdays,
  344. year=self.year or other.year,
  345. month=self.month or other.month,
  346. day=self.day or other.day,
  347. weekday=self.weekday or other.weekday,
  348. hour=self.hour or other.hour,
  349. minute=self.minute or other.minute,
  350. second=self.second or other.second,
  351. microsecond=self.microsecond or other.microsecond)
  352. def __neg__(self):
  353. return self.__class__(years=-self.years,
  354. months=-self.months,
  355. days=-self.days,
  356. hours=-self.hours,
  357. minutes=-self.minutes,
  358. seconds=-self.seconds,
  359. microseconds=-self.microseconds,
  360. leapdays=self.leapdays,
  361. year=self.year,
  362. month=self.month,
  363. day=self.day,
  364. weekday=self.weekday,
  365. hour=self.hour,
  366. minute=self.minute,
  367. second=self.second,
  368. microsecond=self.microsecond)
  369. def __bool__(self):
  370. return not (not self.years and
  371. not self.months and
  372. not self.days and
  373. not self.hours and
  374. not self.minutes and
  375. not self.seconds and
  376. not self.microseconds and
  377. not self.leapdays and
  378. self.year is None and
  379. self.month is None and
  380. self.day is None and
  381. self.weekday is None and
  382. self.hour is None and
  383. self.minute is None and
  384. self.second is None and
  385. self.microsecond is None)
  386. # Compatibility with Python 2.x
  387. __nonzero__ = __bool__
  388. def __mul__(self, other):
  389. try:
  390. f = float(other)
  391. except TypeError:
  392. return NotImplemented
  393. return self.__class__(years=int(self.years * f),
  394. months=int(self.months * f),
  395. days=int(self.days * f),
  396. hours=int(self.hours * f),
  397. minutes=int(self.minutes * f),
  398. seconds=int(self.seconds * f),
  399. microseconds=int(self.microseconds * f),
  400. leapdays=self.leapdays,
  401. year=self.year,
  402. month=self.month,
  403. day=self.day,
  404. weekday=self.weekday,
  405. hour=self.hour,
  406. minute=self.minute,
  407. second=self.second,
  408. microsecond=self.microsecond)
  409. __rmul__ = __mul__
  410. def __eq__(self, other):
  411. if not isinstance(other, relativedelta):
  412. return NotImplemented
  413. if self.weekday or other.weekday:
  414. if not self.weekday or not other.weekday:
  415. return False
  416. if self.weekday.weekday != other.weekday.weekday:
  417. return False
  418. n1, n2 = self.weekday.n, other.weekday.n
  419. if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
  420. return False
  421. return (self.years == other.years and
  422. self.months == other.months and
  423. self.days == other.days and
  424. self.hours == other.hours and
  425. self.minutes == other.minutes and
  426. self.seconds == other.seconds and
  427. self.microseconds == other.microseconds and
  428. self.leapdays == other.leapdays and
  429. self.year == other.year and
  430. self.month == other.month and
  431. self.day == other.day and
  432. self.hour == other.hour and
  433. self.minute == other.minute and
  434. self.second == other.second and
  435. self.microsecond == other.microsecond)
  436. __hash__ = None
  437. def __ne__(self, other):
  438. return not self.__eq__(other)
  439. def __div__(self, other):
  440. try:
  441. reciprocal = 1 / float(other)
  442. except TypeError:
  443. return NotImplemented
  444. return self.__mul__(reciprocal)
  445. __truediv__ = __div__
  446. def __repr__(self):
  447. l = []
  448. for attr in ["years", "months", "days", "leapdays",
  449. "hours", "minutes", "seconds", "microseconds"]:
  450. value = getattr(self, attr)
  451. if value:
  452. l.append("{attr}={value:+g}".format(attr=attr, value=value))
  453. for attr in ["year", "month", "day", "weekday",
  454. "hour", "minute", "second", "microsecond"]:
  455. value = getattr(self, attr)
  456. if value is not None:
  457. l.append("{attr}={value}".format(attr=attr, value=repr(value)))
  458. return "{classname}({attrs})".format(classname=self.__class__.__name__,
  459. attrs=", ".join(l))
  460. def _sign(x):
  461. return int(copysign(1, x))
  462. # vim:ts=4:sw=4:et