Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pendulum/interval.py: 37%
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 copy
4import operator
6from datetime import date
7from datetime import datetime
8from datetime import timedelta
9from typing import TYPE_CHECKING
10from typing import Any
11from typing import Generic
12from typing import TypeVar
13from typing import cast
14from typing import overload
16import pendulum
18from pendulum.constants import MONTHS_PER_YEAR
19from pendulum.duration import Duration
20from pendulum.helpers import precise_diff
23if TYPE_CHECKING:
24 from collections.abc import Iterator
26 from typing_extensions import Self
27 from typing_extensions import SupportsIndex
29 from pendulum.helpers import PreciseDiff
30 from pendulum.locales.locale import Locale
33_T = TypeVar("_T", bound=date)
36class Interval(Duration, Generic[_T]):
37 """
38 An interval of time between two datetimes.
39 """
41 def __new__(cls, start: _T, end: _T, absolute: bool = False) -> Self:
42 if (isinstance(start, datetime) and not isinstance(end, datetime)) or (
43 not isinstance(start, datetime) and isinstance(end, datetime)
44 ):
45 raise ValueError(
46 "Both start and end of an Interval must have the same type"
47 )
49 if (
50 isinstance(start, datetime)
51 and isinstance(end, datetime)
52 and (
53 (start.tzinfo is None and end.tzinfo is not None)
54 or (start.tzinfo is not None and end.tzinfo is None)
55 )
56 ):
57 raise TypeError("can't compare offset-naive and offset-aware datetimes")
59 if absolute and start > end:
60 end, start = start, end
62 _start = start
63 _end = end
64 if isinstance(start, pendulum.DateTime):
65 _start = cast(
66 "_T",
67 datetime(
68 start.year,
69 start.month,
70 start.day,
71 start.hour,
72 start.minute,
73 start.second,
74 start.microsecond,
75 tzinfo=start.tzinfo,
76 fold=start.fold,
77 ),
78 )
79 elif isinstance(start, pendulum.Date):
80 _start = cast("_T", date(start.year, start.month, start.day))
82 if isinstance(end, pendulum.DateTime):
83 _end = cast(
84 "_T",
85 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 )
97 elif isinstance(end, pendulum.Date):
98 _end = cast("_T", date(end.year, end.month, end.day))
100 # Fixing issues with datetime.__sub__()
101 # not handling offsets if the tzinfo is the same
102 if (
103 isinstance(_start, datetime)
104 and isinstance(_end, datetime)
105 and _start.tzinfo is _end.tzinfo
106 ):
107 if _start.tzinfo is not None:
108 offset = cast("timedelta", cast("datetime", start).utcoffset())
109 _start = cast("_T", (_start - offset).replace(tzinfo=None))
111 if isinstance(end, datetime) and _end.tzinfo is not None:
112 offset = cast("timedelta", end.utcoffset())
113 _end = cast("_T", (_end - offset).replace(tzinfo=None))
115 delta: timedelta = _end - _start
117 return super().__new__(cls, seconds=delta.total_seconds())
119 def __init__(self, start: _T, end: _T, absolute: bool = False) -> None:
120 super().__init__()
122 _start: _T
123 if not isinstance(start, pendulum.Date):
124 if isinstance(start, datetime):
125 start = cast("_T", pendulum.instance(start))
126 else:
127 start = cast("_T", pendulum.date(start.year, start.month, start.day))
129 _start = start
130 else:
131 if isinstance(start, pendulum.DateTime):
132 _start = cast(
133 "_T",
134 datetime(
135 start.year,
136 start.month,
137 start.day,
138 start.hour,
139 start.minute,
140 start.second,
141 start.microsecond,
142 tzinfo=start.tzinfo,
143 ),
144 )
145 else:
146 _start = cast("_T", date(start.year, start.month, start.day))
148 _end: _T
149 if not isinstance(end, pendulum.Date):
150 if isinstance(end, datetime):
151 end = cast("_T", pendulum.instance(end))
152 else:
153 end = cast("_T", pendulum.date(end.year, end.month, end.day))
155 _end = end
156 else:
157 if isinstance(end, pendulum.DateTime):
158 _end = cast(
159 "_T",
160 datetime(
161 end.year,
162 end.month,
163 end.day,
164 end.hour,
165 end.minute,
166 end.second,
167 end.microsecond,
168 tzinfo=end.tzinfo,
169 ),
170 )
171 else:
172 _end = cast("_T", date(end.year, end.month, end.day))
174 self._invert = False
175 if start > end:
176 self._invert = True
178 if absolute:
179 end, start = start, end
180 _end, _start = _start, _end
182 self._absolute = absolute
183 self._start: _T = start
184 self._end: _T = end
185 self._delta: PreciseDiff = precise_diff(_start, _end)
187 @property
188 def years(self) -> int:
189 return self._delta.years
191 @property
192 def months(self) -> int:
193 return self._delta.months
195 @property
196 def weeks(self) -> int:
197 return abs(self._delta.days) // 7 * self._sign(self._delta.days)
199 @property
200 def days(self) -> int:
201 return self._days
203 @property
204 def remaining_days(self) -> int:
205 return abs(self._delta.days) % 7 * self._sign(self._days)
207 @property
208 def hours(self) -> int:
209 return self._delta.hours
211 @property
212 def minutes(self) -> int:
213 return self._delta.minutes
215 @property
216 def start(self) -> _T:
217 return self._start
219 @property
220 def end(self) -> _T:
221 return self._end
223 def in_years(self) -> int:
224 """
225 Gives the duration of the Interval in full years.
226 """
227 return self.years
229 def in_months(self) -> int:
230 """
231 Gives the duration of the Interval in full months.
232 """
233 return self.years * MONTHS_PER_YEAR + self.months
235 def in_weeks(self) -> int:
236 days = self.in_days()
237 sign = 1
239 if days < 0:
240 sign = -1
242 return sign * (abs(days) // 7)
244 def in_days(self) -> int:
245 return self._delta.total_days
247 def in_words(self, locale: str | None = None, separator: str = " ") -> str:
248 """
249 Get the current interval in words in the current locale.
251 Ex: 6 jours 23 heures 58 minutes
253 :param locale: The locale to use. Defaults to current locale.
254 :param separator: The separator to use between each unit
255 """
256 from pendulum.locales.locale import Locale
258 intervals = [
259 ("year", self.years),
260 ("month", self.months),
261 ("week", self.weeks),
262 ("day", self.remaining_days),
263 ("hour", self.hours),
264 ("minute", self.minutes),
265 ("second", self.remaining_seconds),
266 ]
267 loaded_locale: Locale = Locale.load(locale or pendulum.get_locale())
268 parts = []
269 for interval in intervals:
270 unit, interval_count = interval
271 if abs(interval_count) > 0:
272 translation = loaded_locale.translation(
273 f"units.{unit}.{loaded_locale.plural(abs(interval_count))}"
274 )
275 parts.append(translation.format(interval_count))
277 if not parts:
278 count: str | int = 0
279 if abs(self.microseconds) > 0:
280 unit = f"units.second.{loaded_locale.plural(1)}"
281 count = f"{abs(self.microseconds) / 1e6:.2f}"
282 else:
283 unit = f"units.microsecond.{loaded_locale.plural(0)}"
285 translation = loaded_locale.translation(unit)
286 parts.append(translation.format(count))
288 return separator.join(parts)
290 def range(self, unit: str, amount: int = 1) -> Iterator[_T]:
291 method = "add"
292 op = operator.le
293 if not self._absolute and self.invert:
294 method = "subtract"
295 op = operator.ge
297 start, end = self.start, self.end
299 i = amount
300 while op(start, end):
301 yield start
303 start = getattr(self.start, method)(**{unit: i})
305 i += amount
307 def as_duration(self) -> Duration:
308 """
309 Return the Interval as a Duration.
310 """
311 return Duration(seconds=self.total_seconds())
313 def __iter__(self) -> Iterator[_T]:
314 return self.range("days")
316 def __contains__(self, item: _T) -> bool:
317 return self.start <= item <= self.end
319 def __add__(self, other: timedelta) -> Duration: # type: ignore[override]
320 return self.as_duration().__add__(other)
322 __radd__ = __add__ # type: ignore[assignment]
324 def __sub__(self, other: timedelta) -> Duration: # type: ignore[override]
325 return self.as_duration().__sub__(other)
327 def __neg__(self) -> Self:
328 return self.__class__(self.end, self.start, self._absolute)
330 def __mul__(self, other: int | float) -> Duration: # type: ignore[override]
331 return self.as_duration().__mul__(other)
333 __rmul__ = __mul__ # type: ignore[assignment]
335 @overload # type: ignore[override]
336 def __floordiv__(self, other: timedelta) -> int: ...
338 @overload
339 def __floordiv__(self, other: int) -> Duration: ...
341 def __floordiv__(self, other: int | timedelta) -> int | Duration:
342 return self.as_duration().__floordiv__(other)
344 __div__ = __floordiv__ # type: ignore[assignment]
346 @overload # type: ignore[override]
347 def __truediv__(self, other: timedelta) -> float: ...
349 @overload
350 def __truediv__(self, other: float) -> Duration: ...
352 def __truediv__(self, other: float | timedelta) -> Duration | float:
353 return self.as_duration().__truediv__(other)
355 def __mod__(self, other: timedelta) -> Duration: # type: ignore[override]
356 return self.as_duration().__mod__(other)
358 def __divmod__(self, other: timedelta) -> tuple[int, Duration]:
359 return self.as_duration().__divmod__(other)
361 def __abs__(self) -> Self:
362 return self.__class__(self.start, self.end, absolute=True)
364 def __repr__(self) -> str:
365 return f"<Interval [{self._start} -> {self._end}]>"
367 def __str__(self) -> str:
368 return self.__repr__()
370 def _cmp(self, other: timedelta) -> int:
371 # Only needed for PyPy
372 assert isinstance(other, timedelta)
374 if isinstance(other, Interval):
375 other = other.as_timedelta()
377 td = self.as_timedelta()
379 return 0 if td == other else 1 if td > other else -1
381 def _getstate(self, protocol: SupportsIndex = 3) -> tuple[_T, _T, bool]:
382 start, end = self.start, self.end
384 if self._invert and self._absolute:
385 end, start = start, end
387 return start, end, self._absolute
389 def __reduce__(
390 self,
391 ) -> tuple[type[Self], tuple[_T, _T, bool]]:
392 return self.__reduce_ex__(2)
394 def __reduce_ex__(
395 self, protocol: SupportsIndex
396 ) -> tuple[type[Self], tuple[_T, _T, bool]]:
397 return self.__class__, self._getstate(protocol)
399 def __hash__(self) -> int:
400 return hash((self.start, self.end, self._absolute))
402 def __eq__(self, other: object) -> bool:
403 if isinstance(other, Interval):
404 return (self.start, self.end, self._absolute) == (
405 other.start,
406 other.end,
407 other._absolute,
408 )
409 else:
410 return self.as_duration() == other
412 def __ne__(self, other: object) -> bool:
413 return not self.__eq__(other)
415 def __deepcopy__(self, memo: dict[int, Any]) -> Self:
416 return self.__class__(
417 copy.deepcopy(self.start, memo),
418 copy.deepcopy(self.end, memo),
419 self._absolute,
420 )