Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pendulum/duration.py: 49%
273 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 06:11 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 06:11 +0000
1from __future__ import annotations
3from datetime import timedelta
4from typing import TYPE_CHECKING
5from typing import cast
6from typing import overload
8import pendulum
10from pendulum.constants import SECONDS_PER_DAY
11from pendulum.constants import SECONDS_PER_HOUR
12from pendulum.constants import SECONDS_PER_MINUTE
13from pendulum.constants import US_PER_SECOND
14from pendulum.utils._compat import PYPY
17if TYPE_CHECKING:
18 from typing_extensions import Self
21def _divide_and_round(a: float, b: float) -> int:
22 """divide a by b and round result to the nearest integer
24 When the ratio is exactly half-way between two integers,
25 the even integer is returned.
26 """
27 # Based on the reference implementation for divmod_near
28 # in Objects/longobject.c.
29 q, r = divmod(a, b)
31 # The output of divmod() is either a float or an int,
32 # but we always want it to be an int.
33 q = int(q)
35 # round up if either r / b > 0.5, or r / b == 0.5 and q is odd.
36 # The expression r / b > 0.5 is equivalent to 2 * r > b if b is
37 # positive, 2 * r < b if b negative.
38 r *= 2
39 greater_than_half = r > b if b > 0 else r < b
40 if greater_than_half or r == b and q % 2 == 1:
41 q += 1
43 return q
46class Duration(timedelta):
47 """
48 Replacement for the standard timedelta class.
50 Provides several improvements over the base class.
51 """
53 _total: float = 0
54 _years: int = 0
55 _months: int = 0
56 _weeks: int = 0
57 _days: int = 0
58 _remaining_days: int = 0
59 _seconds: int = 0
60 _microseconds: int = 0
62 _y = None
63 _m = None
64 _w = None
65 _d = None
66 _h = None
67 _i = None
68 _s = None
69 _invert = None
71 def __new__(
72 cls,
73 days: float = 0,
74 seconds: float = 0,
75 microseconds: float = 0,
76 milliseconds: float = 0,
77 minutes: float = 0,
78 hours: float = 0,
79 weeks: float = 0,
80 years: float = 0,
81 months: float = 0,
82 ) -> Self:
83 if not isinstance(years, int) or not isinstance(months, int):
84 raise ValueError("Float year and months are not supported")
86 self = timedelta.__new__(
87 cls,
88 days + years * 365 + months * 30,
89 seconds,
90 microseconds,
91 milliseconds,
92 minutes,
93 hours,
94 weeks,
95 )
97 # Intuitive normalization
98 total = self.total_seconds() - (years * 365 + months * 30) * SECONDS_PER_DAY
99 self._total = total
101 m = 1
102 if total < 0:
103 m = -1
105 self._microseconds = round(total % m * 1e6)
106 self._seconds = abs(int(total)) % SECONDS_PER_DAY * m
108 _days = abs(int(total)) // SECONDS_PER_DAY * m
109 self._days = _days
110 self._remaining_days = abs(_days) % 7 * m
111 self._weeks = abs(_days) // 7 * m
112 self._months = months
113 self._years = years
115 return self
117 def total_minutes(self) -> float:
118 return self.total_seconds() / SECONDS_PER_MINUTE
120 def total_hours(self) -> float:
121 return self.total_seconds() / SECONDS_PER_HOUR
123 def total_days(self) -> float:
124 return self.total_seconds() / SECONDS_PER_DAY
126 def total_weeks(self) -> float:
127 return self.total_days() / 7
129 if PYPY:
131 def total_seconds(self) -> float:
132 days = 0
134 if hasattr(self, "_years"):
135 days += self._years * 365
137 if hasattr(self, "_months"):
138 days += self._months * 30
140 if hasattr(self, "_remaining_days"):
141 days += self._weeks * 7 + self._remaining_days
142 else:
143 days += self._days
145 return (
146 (days * SECONDS_PER_DAY + self._seconds) * US_PER_SECOND
147 + self._microseconds
148 ) / US_PER_SECOND
150 @property
151 def years(self) -> int:
152 return self._years
154 @property
155 def months(self) -> int:
156 return self._months
158 @property
159 def weeks(self) -> int:
160 return self._weeks
162 if PYPY:
164 @property
165 def days(self) -> int:
166 return self._years * 365 + self._months * 30 + self._days
168 @property
169 def remaining_days(self) -> int:
170 return self._remaining_days
172 @property
173 def hours(self) -> int:
174 if self._h is None:
175 seconds = self._seconds
176 self._h = 0
177 if abs(seconds) >= 3600:
178 self._h = (abs(seconds) // 3600 % 24) * self._sign(seconds)
180 return self._h
182 @property
183 def minutes(self) -> int:
184 if self._i is None:
185 seconds = self._seconds
186 self._i = 0
187 if abs(seconds) >= 60:
188 self._i = (abs(seconds) // 60 % 60) * self._sign(seconds)
190 return self._i
192 @property
193 def seconds(self) -> int:
194 return self._seconds
196 @property
197 def remaining_seconds(self) -> int:
198 if self._s is None:
199 self._s = self._seconds
200 self._s = abs(self._s) % 60 * self._sign(self._s)
202 return self._s
204 @property
205 def microseconds(self) -> int:
206 return self._microseconds
208 @property
209 def invert(self) -> bool:
210 if self._invert is None:
211 self._invert = self.total_seconds() < 0
213 return self._invert
215 def in_weeks(self) -> int:
216 return int(self.total_weeks())
218 def in_days(self) -> int:
219 return int(self.total_days())
221 def in_hours(self) -> int:
222 return int(self.total_hours())
224 def in_minutes(self) -> int:
225 return int(self.total_minutes())
227 def in_seconds(self) -> int:
228 return int(self.total_seconds())
230 def in_words(self, locale: str | None = None, separator: str = " ") -> str:
231 """
232 Get the current interval in words in the current locale.
234 Ex: 6 jours 23 heures 58 minutes
236 :param locale: The locale to use. Defaults to current locale.
237 :param separator: The separator to use between each unit
238 """
239 periods = [
240 ("year", self.years),
241 ("month", self.months),
242 ("week", self.weeks),
243 ("day", self.remaining_days),
244 ("hour", self.hours),
245 ("minute", self.minutes),
246 ("second", self.remaining_seconds),
247 ]
249 if locale is None:
250 locale = pendulum.get_locale()
252 loaded_locale = pendulum.locale(locale)
254 parts = []
255 for period in periods:
256 unit, period_count = period
257 if abs(period_count) > 0:
258 translation = loaded_locale.translation(
259 f"units.{unit}.{loaded_locale.plural(abs(period_count))}"
260 )
261 parts.append(translation.format(period_count))
263 if not parts:
264 count: int | str = 0
265 if abs(self.microseconds) > 0:
266 unit = f"units.second.{loaded_locale.plural(1)}"
267 count = f"{abs(self.microseconds) / 1e6:.2f}"
268 else:
269 unit = f"units.microsecond.{loaded_locale.plural(0)}"
270 translation = loaded_locale.translation(unit)
271 parts.append(translation.format(count))
273 return separator.join(parts)
275 def _sign(self, value: float) -> int:
276 if value < 0:
277 return -1
279 return 1
281 def as_timedelta(self) -> timedelta:
282 """
283 Return the interval as a native timedelta.
284 """
285 return timedelta(seconds=self.total_seconds())
287 def __str__(self) -> str:
288 return self.in_words()
290 def __repr__(self) -> str:
291 rep = f"{self.__class__.__name__}("
293 if self._years:
294 rep += f"years={self._years}, "
296 if self._months:
297 rep += f"months={self._months}, "
299 if self._weeks:
300 rep += f"weeks={self._weeks}, "
302 if self._days:
303 rep += f"days={self._remaining_days}, "
305 if self.hours:
306 rep += f"hours={self.hours}, "
308 if self.minutes:
309 rep += f"minutes={self.minutes}, "
311 if self.remaining_seconds:
312 rep += f"seconds={self.remaining_seconds}, "
314 if self.microseconds:
315 rep += f"microseconds={self.microseconds}, "
317 rep += ")"
319 return rep.replace(", )", ")")
321 def __add__(self, other: timedelta) -> Self:
322 if isinstance(other, timedelta):
323 return self.__class__(seconds=self.total_seconds() + other.total_seconds())
325 return NotImplemented
327 __radd__ = __add__
329 def __sub__(self, other: timedelta) -> Self:
330 if isinstance(other, timedelta):
331 return self.__class__(seconds=self.total_seconds() - other.total_seconds())
333 return NotImplemented
335 def __neg__(self) -> Self:
336 return self.__class__(
337 years=-self._years,
338 months=-self._months,
339 weeks=-self._weeks,
340 days=-self._remaining_days,
341 seconds=-self._seconds,
342 microseconds=-self._microseconds,
343 )
345 def _to_microseconds(self) -> int:
346 return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds
348 def __mul__(self, other: int | float) -> Self:
349 if isinstance(other, int):
350 return self.__class__(
351 years=self._years * other,
352 months=self._months * other,
353 seconds=self._total * other,
354 )
356 if isinstance(other, float):
357 usec = self._to_microseconds()
358 a, b = other.as_integer_ratio()
360 return self.__class__(0, 0, _divide_and_round(usec * a, b))
362 return NotImplemented
364 __rmul__ = __mul__
366 @overload
367 def __floordiv__(self, other: timedelta) -> int:
368 ...
370 @overload
371 def __floordiv__(self, other: int) -> Self:
372 ...
374 def __floordiv__(self, other: int | timedelta) -> int | Duration:
375 if not isinstance(other, (int, timedelta)):
376 return NotImplemented
378 usec = self._to_microseconds()
379 if isinstance(other, timedelta):
380 return cast(
381 int, usec // other._to_microseconds() # type: ignore[attr-defined]
382 )
384 if isinstance(other, int):
385 return self.__class__(
386 0,
387 0,
388 usec // other,
389 years=self._years // other,
390 months=self._months // other,
391 )
393 @overload
394 def __truediv__(self, other: timedelta) -> float:
395 ...
397 @overload
398 def __truediv__(self, other: float) -> Self:
399 ...
401 def __truediv__(self, other: int | float | timedelta) -> Self | float:
402 if not isinstance(other, (int, float, timedelta)):
403 return NotImplemented
405 usec = self._to_microseconds()
406 if isinstance(other, timedelta):
407 return cast(
408 float, usec / other._to_microseconds() # type: ignore[attr-defined]
409 )
411 if isinstance(other, int):
412 return self.__class__(
413 0,
414 0,
415 _divide_and_round(usec, other),
416 years=_divide_and_round(self._years, other),
417 months=_divide_and_round(self._months, other),
418 )
420 if isinstance(other, float):
421 a, b = other.as_integer_ratio()
423 return self.__class__(
424 0,
425 0,
426 _divide_and_round(b * usec, a),
427 years=_divide_and_round(self._years * b, a),
428 months=_divide_and_round(self._months, other),
429 )
431 __div__ = __floordiv__
433 def __mod__(self, other: timedelta) -> Self:
434 if isinstance(other, timedelta):
435 r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined] # noqa: E501
437 return self.__class__(0, 0, r)
439 return NotImplemented
441 def __divmod__(self, other: timedelta) -> tuple[int, Duration]:
442 if isinstance(other, timedelta):
443 q, r = divmod(self._to_microseconds(), other._to_microseconds()) # type: ignore[attr-defined] # noqa: E501
445 return q, self.__class__(0, 0, r)
447 return NotImplemented
449 def __deepcopy__(self, _: dict[int, Self]) -> Self:
450 return self.__class__(
451 days=self.remaining_days,
452 seconds=self.remaining_seconds,
453 microseconds=self.microseconds,
454 minutes=self.minutes,
455 hours=self.hours,
456 years=self.years,
457 months=self.months,
458 )
461Duration.min = Duration(days=-999999999)
462Duration.max = Duration(
463 days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999
464)
465Duration.resolution = Duration(microseconds=1)
468class AbsoluteDuration(Duration):
469 """
470 Duration that expresses a time difference in absolute values.
471 """
473 def __new__(
474 cls,
475 days: float = 0,
476 seconds: float = 0,
477 microseconds: float = 0,
478 milliseconds: float = 0,
479 minutes: float = 0,
480 hours: float = 0,
481 weeks: float = 0,
482 years: float = 0,
483 months: float = 0,
484 ) -> AbsoluteDuration:
485 if not isinstance(years, int) or not isinstance(months, int):
486 raise ValueError("Float year and months are not supported")
488 self = timedelta.__new__(
489 cls, days, seconds, microseconds, milliseconds, minutes, hours, weeks
490 )
492 # We need to compute the total_seconds() value
493 # on a native timedelta object
494 delta = timedelta(
495 days, seconds, microseconds, milliseconds, minutes, hours, weeks
496 )
498 # Intuitive normalization
499 self._total = delta.total_seconds()
500 total = abs(self._total)
502 self._microseconds = round(total % 1 * 1e6)
503 days, self._seconds = divmod(int(total), SECONDS_PER_DAY)
504 self._days = abs(days + years * 365 + months * 30)
505 self._weeks, self._remaining_days = divmod(days, 7)
506 self._months = abs(months)
507 self._years = abs(years)
509 return self
511 def total_seconds(self) -> float:
512 return abs(self._total)
514 @property
515 def invert(self) -> bool:
516 if self._invert is None:
517 self._invert = self._total < 0
519 return self._invert