interval.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. from __future__ import annotations
  2. import operator
  3. from datetime import date
  4. from datetime import datetime
  5. from datetime import timedelta
  6. from typing import TYPE_CHECKING
  7. from typing import Iterator
  8. from typing import Union
  9. from typing import cast
  10. from typing import overload
  11. import pendulum
  12. from pendulum.constants import MONTHS_PER_YEAR
  13. from pendulum.duration import Duration
  14. from pendulum.helpers import precise_diff
  15. if TYPE_CHECKING:
  16. from typing_extensions import Self
  17. from typing_extensions import SupportsIndex
  18. from pendulum.helpers import PreciseDiff
  19. from pendulum.locales.locale import Locale
  20. class Interval(Duration):
  21. """
  22. An interval of time between two datetimes.
  23. """
  24. @overload
  25. def __new__(
  26. cls,
  27. start: pendulum.DateTime | datetime,
  28. end: pendulum.DateTime | datetime,
  29. absolute: bool = False,
  30. ) -> Self:
  31. ...
  32. @overload
  33. def __new__(
  34. cls,
  35. start: pendulum.Date | date,
  36. end: pendulum.Date | date,
  37. absolute: bool = False,
  38. ) -> Self:
  39. ...
  40. def __new__(
  41. cls,
  42. start: pendulum.DateTime | pendulum.Date | datetime | date,
  43. end: pendulum.DateTime | pendulum.Date | datetime | date,
  44. absolute: bool = False,
  45. ) -> Self:
  46. if (
  47. isinstance(start, datetime)
  48. and not isinstance(end, datetime)
  49. or not isinstance(start, datetime)
  50. and isinstance(end, datetime)
  51. ):
  52. raise ValueError(
  53. "Both start and end of an Interval must have the same type"
  54. )
  55. if (
  56. isinstance(start, datetime)
  57. and isinstance(end, datetime)
  58. and (
  59. start.tzinfo is None
  60. and end.tzinfo is not None
  61. or start.tzinfo is not None
  62. and end.tzinfo is None
  63. )
  64. ):
  65. raise TypeError("can't compare offset-naive and offset-aware datetimes")
  66. if absolute and start > end:
  67. end, start = start, end
  68. _start = start
  69. _end = end
  70. if isinstance(start, pendulum.DateTime):
  71. _start = datetime(
  72. start.year,
  73. start.month,
  74. start.day,
  75. start.hour,
  76. start.minute,
  77. start.second,
  78. start.microsecond,
  79. tzinfo=start.tzinfo,
  80. fold=start.fold,
  81. )
  82. elif isinstance(start, pendulum.Date):
  83. _start = date(start.year, start.month, start.day)
  84. if isinstance(end, pendulum.DateTime):
  85. _end = datetime(
  86. end.year,
  87. end.month,
  88. end.day,
  89. end.hour,
  90. end.minute,
  91. end.second,
  92. end.microsecond,
  93. tzinfo=end.tzinfo,
  94. fold=end.fold,
  95. )
  96. elif isinstance(end, pendulum.Date):
  97. _end = date(end.year, end.month, end.day)
  98. # Fixing issues with datetime.__sub__()
  99. # not handling offsets if the tzinfo is the same
  100. if (
  101. isinstance(_start, datetime)
  102. and isinstance(_end, datetime)
  103. and _start.tzinfo is _end.tzinfo
  104. ):
  105. if _start.tzinfo is not None:
  106. offset = cast(timedelta, cast(datetime, start).utcoffset())
  107. _start = (_start - offset).replace(tzinfo=None)
  108. if isinstance(end, datetime) and _end.tzinfo is not None:
  109. offset = cast(timedelta, end.utcoffset())
  110. _end = (_end - offset).replace(tzinfo=None)
  111. delta: timedelta = _end - _start # type: ignore[operator]
  112. return super().__new__(cls, seconds=delta.total_seconds())
  113. def __init__(
  114. self,
  115. start: pendulum.DateTime | pendulum.Date | datetime | date,
  116. end: pendulum.DateTime | pendulum.Date | datetime | date,
  117. absolute: bool = False,
  118. ) -> None:
  119. super().__init__()
  120. _start: pendulum.DateTime | pendulum.Date | datetime | date
  121. if not isinstance(start, pendulum.Date):
  122. if isinstance(start, datetime):
  123. start = pendulum.instance(start)
  124. else:
  125. start = pendulum.date(start.year, start.month, start.day)
  126. _start = start
  127. else:
  128. if isinstance(start, pendulum.DateTime):
  129. _start = datetime(
  130. start.year,
  131. start.month,
  132. start.day,
  133. start.hour,
  134. start.minute,
  135. start.second,
  136. start.microsecond,
  137. tzinfo=start.tzinfo,
  138. )
  139. else:
  140. _start = date(start.year, start.month, start.day)
  141. _end: pendulum.DateTime | pendulum.Date | datetime | date
  142. if not isinstance(end, pendulum.Date):
  143. if isinstance(end, datetime):
  144. end = pendulum.instance(end)
  145. else:
  146. end = pendulum.date(end.year, end.month, end.day)
  147. _end = end
  148. else:
  149. if isinstance(end, pendulum.DateTime):
  150. _end = datetime(
  151. end.year,
  152. end.month,
  153. end.day,
  154. end.hour,
  155. end.minute,
  156. end.second,
  157. end.microsecond,
  158. tzinfo=end.tzinfo,
  159. )
  160. else:
  161. _end = date(end.year, end.month, end.day)
  162. self._invert = False
  163. if start > end:
  164. self._invert = True
  165. if absolute:
  166. end, start = start, end
  167. _end, _start = _start, _end
  168. self._absolute = absolute
  169. self._start: pendulum.DateTime | pendulum.Date = start
  170. self._end: pendulum.DateTime | pendulum.Date = end
  171. self._delta: PreciseDiff = precise_diff(_start, _end)
  172. @property
  173. def years(self) -> int:
  174. return self._delta.years
  175. @property
  176. def months(self) -> int:
  177. return self._delta.months
  178. @property
  179. def weeks(self) -> int:
  180. return abs(self._delta.days) // 7 * self._sign(self._delta.days)
  181. @property
  182. def days(self) -> int:
  183. return self._days
  184. @property
  185. def remaining_days(self) -> int:
  186. return abs(self._delta.days) % 7 * self._sign(self._days)
  187. @property
  188. def hours(self) -> int:
  189. return self._delta.hours
  190. @property
  191. def minutes(self) -> int:
  192. return self._delta.minutes
  193. @property
  194. def start(self) -> pendulum.DateTime | pendulum.Date | datetime | date:
  195. return self._start
  196. @property
  197. def end(self) -> pendulum.DateTime | pendulum.Date | datetime | date:
  198. return self._end
  199. def in_years(self) -> int:
  200. """
  201. Gives the duration of the Interval in full years.
  202. """
  203. return self.years
  204. def in_months(self) -> int:
  205. """
  206. Gives the duration of the Interval in full months.
  207. """
  208. return self.years * MONTHS_PER_YEAR + self.months
  209. def in_weeks(self) -> int:
  210. days = self.in_days()
  211. sign = 1
  212. if days < 0:
  213. sign = -1
  214. return sign * (abs(days) // 7)
  215. def in_days(self) -> int:
  216. return self._delta.total_days
  217. def in_words(self, locale: str | None = None, separator: str = " ") -> str:
  218. """
  219. Get the current interval in words in the current locale.
  220. Ex: 6 jours 23 heures 58 minutes
  221. :param locale: The locale to use. Defaults to current locale.
  222. :param separator: The separator to use between each unit
  223. """
  224. from pendulum.locales.locale import Locale
  225. intervals = [
  226. ("year", self.years),
  227. ("month", self.months),
  228. ("week", self.weeks),
  229. ("day", self.remaining_days),
  230. ("hour", self.hours),
  231. ("minute", self.minutes),
  232. ("second", self.remaining_seconds),
  233. ]
  234. loaded_locale: Locale = Locale.load(locale or pendulum.get_locale())
  235. parts = []
  236. for interval in intervals:
  237. unit, interval_count = interval
  238. if abs(interval_count) > 0:
  239. translation = loaded_locale.translation(
  240. f"units.{unit}.{loaded_locale.plural(abs(interval_count))}"
  241. )
  242. parts.append(translation.format(interval_count))
  243. if not parts:
  244. count: str | int = 0
  245. if abs(self.microseconds) > 0:
  246. unit = f"units.second.{loaded_locale.plural(1)}"
  247. count = f"{abs(self.microseconds) / 1e6:.2f}"
  248. else:
  249. unit = f"units.microsecond.{loaded_locale.plural(0)}"
  250. translation = loaded_locale.translation(unit)
  251. parts.append(translation.format(count))
  252. return separator.join(parts)
  253. def range(
  254. self, unit: str, amount: int = 1
  255. ) -> Iterator[pendulum.DateTime | pendulum.Date]:
  256. method = "add"
  257. op = operator.le
  258. if not self._absolute and self.invert:
  259. method = "subtract"
  260. op = operator.ge
  261. start, end = self.start, self.end
  262. i = amount
  263. while op(start, end):
  264. yield cast(Union[pendulum.DateTime, pendulum.Date], start)
  265. start = getattr(self.start, method)(**{unit: i})
  266. i += amount
  267. def as_duration(self) -> Duration:
  268. """
  269. Return the Interval as a Duration.
  270. """
  271. return Duration(seconds=self.total_seconds())
  272. def __iter__(self) -> Iterator[pendulum.DateTime | pendulum.Date]:
  273. return self.range("days")
  274. def __contains__(
  275. self, item: datetime | date | pendulum.DateTime | pendulum.Date
  276. ) -> bool:
  277. return self.start <= item <= self.end
  278. def __add__(self, other: timedelta) -> Duration: # type: ignore[override]
  279. return self.as_duration().__add__(other)
  280. __radd__ = __add__ # type: ignore[assignment]
  281. def __sub__(self, other: timedelta) -> Duration: # type: ignore[override]
  282. return self.as_duration().__sub__(other)
  283. def __neg__(self) -> Self:
  284. return self.__class__(self.end, self.start, self._absolute)
  285. def __mul__(self, other: int | float) -> Duration: # type: ignore[override]
  286. return self.as_duration().__mul__(other)
  287. __rmul__ = __mul__ # type: ignore[assignment]
  288. @overload # type: ignore[override]
  289. def __floordiv__(self, other: timedelta) -> int:
  290. ...
  291. @overload
  292. def __floordiv__(self, other: int) -> Duration:
  293. ...
  294. def __floordiv__(self, other: int | timedelta) -> int | Duration:
  295. return self.as_duration().__floordiv__(other)
  296. __div__ = __floordiv__ # type: ignore[assignment]
  297. @overload # type: ignore[override]
  298. def __truediv__(self, other: timedelta) -> float:
  299. ...
  300. @overload
  301. def __truediv__(self, other: float) -> Duration:
  302. ...
  303. def __truediv__(self, other: float | timedelta) -> Duration | float:
  304. return self.as_duration().__truediv__(other)
  305. def __mod__(self, other: timedelta) -> Duration: # type: ignore[override]
  306. return self.as_duration().__mod__(other)
  307. def __divmod__(self, other: timedelta) -> tuple[int, Duration]:
  308. return self.as_duration().__divmod__(other)
  309. def __abs__(self) -> Self:
  310. return self.__class__(self.start, self.end, absolute=True)
  311. def __repr__(self) -> str:
  312. return f"<Interval [{self._start} -> {self._end}]>"
  313. def __str__(self) -> str:
  314. return self.__repr__()
  315. def _cmp(self, other: timedelta) -> int:
  316. # Only needed for PyPy
  317. assert isinstance(other, timedelta)
  318. if isinstance(other, Interval):
  319. other = other.as_timedelta()
  320. td = self.as_timedelta()
  321. return 0 if td == other else 1 if td > other else -1
  322. def _getstate(
  323. self, protocol: SupportsIndex = 3
  324. ) -> tuple[
  325. pendulum.DateTime | pendulum.Date | datetime | date,
  326. pendulum.DateTime | pendulum.Date | datetime | date,
  327. bool,
  328. ]:
  329. start, end = self.start, self.end
  330. if self._invert and self._absolute:
  331. end, start = start, end
  332. return start, end, self._absolute
  333. def __reduce__(
  334. self,
  335. ) -> tuple[
  336. type[Self],
  337. tuple[
  338. pendulum.DateTime | pendulum.Date | datetime | date,
  339. pendulum.DateTime | pendulum.Date | datetime | date,
  340. bool,
  341. ],
  342. ]:
  343. return self.__reduce_ex__(2)
  344. def __reduce_ex__(
  345. self, protocol: SupportsIndex
  346. ) -> tuple[
  347. type[Self],
  348. tuple[
  349. pendulum.DateTime | pendulum.Date | datetime | date,
  350. pendulum.DateTime | pendulum.Date | datetime | date,
  351. bool,
  352. ],
  353. ]:
  354. return self.__class__, self._getstate(protocol)
  355. def __hash__(self) -> int:
  356. return hash((self.start, self.end, self._absolute))
  357. def __eq__(self, other: object) -> bool:
  358. if isinstance(other, Interval):
  359. return (self.start, self.end, self._absolute) == (
  360. other.start,
  361. other.end,
  362. other._absolute,
  363. )
  364. else:
  365. return self.as_duration() == other
  366. def __ne__(self, other: object) -> bool:
  367. return not self.__eq__(other)