Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pendulum/interval.py: 36%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import operator
5from datetime import date
6from datetime import datetime
7from datetime import timedelta
8from typing import TYPE_CHECKING
9from typing import Generic
10from typing import TypeVar
11from typing import cast
12from typing import overload
14import pendulum
16from pendulum.constants import MONTHS_PER_YEAR
17from pendulum.duration import Duration
18from pendulum.helpers import precise_diff
21if TYPE_CHECKING:
22 from collections.abc import Iterator
24 from typing_extensions import Self
25 from typing_extensions import SupportsIndex
27 from pendulum.helpers import PreciseDiff
28 from pendulum.locales.locale import Locale
31_T = TypeVar("_T", bound=date)
34class Interval(Duration, Generic[_T]):
35 """
36 An interval of time between two datetimes.
37 """
39 def __new__(cls, start: _T, end: _T, absolute: bool = False) -> Self:
40 if (isinstance(start, datetime) and not isinstance(end, datetime)) or (
41 not isinstance(start, datetime) and isinstance(end, datetime)
42 ):
43 raise ValueError(
44 "Both start and end of an Interval must have the same type"
45 )
47 if (
48 isinstance(start, datetime)
49 and isinstance(end, datetime)
50 and (
51 (start.tzinfo is None and end.tzinfo is not None)
52 or (start.tzinfo is not None and end.tzinfo is None)
53 )
54 ):
55 raise TypeError("can't compare offset-naive and offset-aware datetimes")
57 if absolute and start > end:
58 end, start = start, end
60 _start = start
61 _end = end
62 if isinstance(start, pendulum.DateTime):
63 _start = cast(
64 "_T",
65 datetime(
66 start.year,
67 start.month,
68 start.day,
69 start.hour,
70 start.minute,
71 start.second,
72 start.microsecond,
73 tzinfo=start.tzinfo,
74 fold=start.fold,
75 ),
76 )
77 elif isinstance(start, pendulum.Date):
78 _start = cast("_T", date(start.year, start.month, start.day))
80 if isinstance(end, pendulum.DateTime):
81 _end = cast(
82 "_T",
83 datetime(
84 end.year,
85 end.month,
86 end.day,
87 end.hour,
88 end.minute,
89 end.second,
90 end.microsecond,
91 tzinfo=end.tzinfo,
92 fold=end.fold,
93 ),
94 )
95 elif isinstance(end, pendulum.Date):
96 _end = cast("_T", 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 = cast("_T", (_start - offset).replace(tzinfo=None))
109 if isinstance(end, datetime) and _end.tzinfo is not None:
110 offset = cast("timedelta", end.utcoffset())
111 _end = cast("_T", (_end - offset).replace(tzinfo=None))
113 delta: timedelta = _end - _start
115 return super().__new__(cls, seconds=delta.total_seconds())
117 def __init__(self, start: _T, end: _T, absolute: bool = False) -> None:
118 super().__init__()
120 _start: _T
121 if not isinstance(start, pendulum.Date):
122 if isinstance(start, datetime):
123 start = cast("_T", pendulum.instance(start))
124 else:
125 start = cast("_T", pendulum.date(start.year, start.month, start.day))
127 _start = start
128 else:
129 if isinstance(start, pendulum.DateTime):
130 _start = cast(
131 "_T",
132 datetime(
133 start.year,
134 start.month,
135 start.day,
136 start.hour,
137 start.minute,
138 start.second,
139 start.microsecond,
140 tzinfo=start.tzinfo,
141 ),
142 )
143 else:
144 _start = cast("_T", date(start.year, start.month, start.day))
146 _end: _T
147 if not isinstance(end, pendulum.Date):
148 if isinstance(end, datetime):
149 end = cast("_T", pendulum.instance(end))
150 else:
151 end = cast("_T", pendulum.date(end.year, end.month, end.day))
153 _end = end
154 else:
155 if isinstance(end, pendulum.DateTime):
156 _end = cast(
157 "_T",
158 datetime(
159 end.year,
160 end.month,
161 end.day,
162 end.hour,
163 end.minute,
164 end.second,
165 end.microsecond,
166 tzinfo=end.tzinfo,
167 ),
168 )
169 else:
170 _end = cast("_T", date(end.year, end.month, end.day))
172 self._invert = False
173 if start > end:
174 self._invert = True
176 if absolute:
177 end, start = start, end
178 _end, _start = _start, _end
180 self._absolute = absolute
181 self._start: _T = start
182 self._end: _T = end
183 self._delta: PreciseDiff = precise_diff(_start, _end)
185 @property
186 def years(self) -> int:
187 return self._delta.years
189 @property
190 def months(self) -> int:
191 return self._delta.months
193 @property
194 def weeks(self) -> int:
195 return abs(self._delta.days) // 7 * self._sign(self._delta.days)
197 @property
198 def days(self) -> int:
199 return self._days
201 @property
202 def remaining_days(self) -> int:
203 return abs(self._delta.days) % 7 * self._sign(self._days)
205 @property
206 def hours(self) -> int:
207 return self._delta.hours
209 @property
210 def minutes(self) -> int:
211 return self._delta.minutes
213 @property
214 def start(self) -> _T:
215 return self._start
217 @property
218 def end(self) -> _T:
219 return self._end
221 def in_years(self) -> int:
222 """
223 Gives the duration of the Interval in full years.
224 """
225 return self.years
227 def in_months(self) -> int:
228 """
229 Gives the duration of the Interval in full months.
230 """
231 return self.years * MONTHS_PER_YEAR + self.months
233 def in_weeks(self) -> int:
234 days = self.in_days()
235 sign = 1
237 if days < 0:
238 sign = -1
240 return sign * (abs(days) // 7)
242 def in_days(self) -> int:
243 return self._delta.total_days
245 def in_words(self, locale: str | None = None, separator: str = " ") -> str:
246 """
247 Get the current interval in words in the current locale.
249 Ex: 6 jours 23 heures 58 minutes
251 :param locale: The locale to use. Defaults to current locale.
252 :param separator: The separator to use between each unit
253 """
254 from pendulum.locales.locale import Locale
256 intervals = [
257 ("year", self.years),
258 ("month", self.months),
259 ("week", self.weeks),
260 ("day", self.remaining_days),
261 ("hour", self.hours),
262 ("minute", self.minutes),
263 ("second", self.remaining_seconds),
264 ]
265 loaded_locale: Locale = Locale.load(locale or pendulum.get_locale())
266 parts = []
267 for interval in intervals:
268 unit, interval_count = interval
269 if abs(interval_count) > 0:
270 translation = loaded_locale.translation(
271 f"units.{unit}.{loaded_locale.plural(abs(interval_count))}"
272 )
273 parts.append(translation.format(interval_count))
275 if not parts:
276 count: str | int = 0
277 if abs(self.microseconds) > 0:
278 unit = f"units.second.{loaded_locale.plural(1)}"
279 count = f"{abs(self.microseconds) / 1e6:.2f}"
280 else:
281 unit = f"units.microsecond.{loaded_locale.plural(0)}"
283 translation = loaded_locale.translation(unit)
284 parts.append(translation.format(count))
286 return separator.join(parts)
288 def range(self, unit: str, amount: int = 1) -> Iterator[_T]:
289 method = "add"
290 op = operator.le
291 if not self._absolute and self.invert:
292 method = "subtract"
293 op = operator.ge
295 start, end = self.start, self.end
297 i = amount
298 while op(start, end):
299 yield start
301 start = getattr(self.start, method)(**{unit: i})
303 i += amount
305 def as_duration(self) -> Duration:
306 """
307 Return the Interval as a Duration.
308 """
309 return Duration(seconds=self.total_seconds())
311 def __iter__(self) -> Iterator[_T]:
312 return self.range("days")
314 def __contains__(self, item: _T) -> bool:
315 return self.start <= item <= self.end
317 def __add__(self, other: timedelta) -> Duration: # type: ignore[override]
318 return self.as_duration().__add__(other)
320 __radd__ = __add__ # type: ignore[assignment]
322 def __sub__(self, other: timedelta) -> Duration: # type: ignore[override]
323 return self.as_duration().__sub__(other)
325 def __neg__(self) -> Self:
326 return self.__class__(self.end, self.start, self._absolute)
328 def __mul__(self, other: int | float) -> Duration: # type: ignore[override]
329 return self.as_duration().__mul__(other)
331 __rmul__ = __mul__ # type: ignore[assignment]
333 @overload # type: ignore[override]
334 def __floordiv__(self, other: timedelta) -> int: ...
336 @overload
337 def __floordiv__(self, other: int) -> Duration: ...
339 def __floordiv__(self, other: int | timedelta) -> int | Duration:
340 return self.as_duration().__floordiv__(other)
342 __div__ = __floordiv__ # type: ignore[assignment]
344 @overload # type: ignore[override]
345 def __truediv__(self, other: timedelta) -> float: ...
347 @overload
348 def __truediv__(self, other: float) -> Duration: ...
350 def __truediv__(self, other: float | timedelta) -> Duration | float:
351 return self.as_duration().__truediv__(other)
353 def __mod__(self, other: timedelta) -> Duration: # type: ignore[override]
354 return self.as_duration().__mod__(other)
356 def __divmod__(self, other: timedelta) -> tuple[int, Duration]:
357 return self.as_duration().__divmod__(other)
359 def __abs__(self) -> Self:
360 return self.__class__(self.start, self.end, absolute=True)
362 def __repr__(self) -> str:
363 return f"<Interval [{self._start} -> {self._end}]>"
365 def __str__(self) -> str:
366 return self.__repr__()
368 def _cmp(self, other: timedelta) -> int:
369 # Only needed for PyPy
370 assert isinstance(other, timedelta)
372 if isinstance(other, Interval):
373 other = other.as_timedelta()
375 td = self.as_timedelta()
377 return 0 if td == other else 1 if td > other else -1
379 def _getstate(self, protocol: SupportsIndex = 3) -> tuple[_T, _T, bool]:
380 start, end = self.start, self.end
382 if self._invert and self._absolute:
383 end, start = start, end
385 return start, end, self._absolute
387 def __reduce__(
388 self,
389 ) -> tuple[type[Self], tuple[_T, _T, bool]]:
390 return self.__reduce_ex__(2)
392 def __reduce_ex__(
393 self, protocol: SupportsIndex
394 ) -> tuple[type[Self], tuple[_T, _T, bool]]:
395 return self.__class__, self._getstate(protocol)
397 def __hash__(self) -> int:
398 return hash((self.start, self.end, self._absolute))
400 def __eq__(self, other: object) -> bool:
401 if isinstance(other, Interval):
402 return (self.start, self.end, self._absolute) == (
403 other.start,
404 other.end,
405 other._absolute,
406 )
407 else:
408 return self.as_duration() == other
410 def __ne__(self, other: object) -> bool:
411 return not self.__eq__(other)