diff options
Diffstat (limited to 'pendulum/interval.py')
-rw-r--r-- | pendulum/interval.py | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/pendulum/interval.py b/pendulum/interval.py new file mode 100644 index 0000000..f20042b --- /dev/null +++ b/pendulum/interval.py @@ -0,0 +1,448 @@ +from __future__ import annotations + +import operator + +from datetime import date +from datetime import datetime +from datetime import timedelta +from typing import TYPE_CHECKING +from typing import Iterator +from typing import Union +from typing import cast +from typing import overload + +import pendulum + +from pendulum.constants import MONTHS_PER_YEAR +from pendulum.duration import Duration +from pendulum.helpers import precise_diff + +if TYPE_CHECKING: + from typing import SupportsIndex + + from pendulum.helpers import PreciseDiff + from pendulum.locales.locale import Locale # noqa + + +class Interval(Duration): + """ + A period of time between two datetimes. + """ + + @overload + def __new__( + cls, + start: pendulum.DateTime | datetime, + end: pendulum.DateTime | datetime, + absolute: bool = False, + ) -> Interval: + ... + + @overload + def __new__( + cls, + start: pendulum.Date | date, + end: pendulum.Date | date, + absolute: bool = False, + ) -> Interval: + ... + + def __new__( + cls, + start: pendulum.DateTime | pendulum.Date | datetime | date, + end: pendulum.DateTime | pendulum.Date | datetime | date, + absolute: bool = False, + ) -> Interval: + if ( + isinstance(start, datetime) + and not isinstance(end, datetime) + or not isinstance(start, datetime) + and isinstance(end, datetime) + ): + raise ValueError("Both start and end of a Period must have the same type") + + if ( + isinstance(start, datetime) + and isinstance(end, datetime) + and ( + start.tzinfo is None + and end.tzinfo is not None + or start.tzinfo is not None + and end.tzinfo is None + ) + ): + raise TypeError("can't compare offset-naive and offset-aware datetimes") + + if absolute and start > end: + end, start = start, end + + _start = start + _end = end + if isinstance(start, pendulum.DateTime): + _start = datetime( + start.year, + start.month, + start.day, + start.hour, + start.minute, + start.second, + start.microsecond, + tzinfo=start.tzinfo, + fold=start.fold, + ) + elif isinstance(start, pendulum.Date): + _start = date(start.year, start.month, start.day) + + if isinstance(end, pendulum.DateTime): + _end = datetime( + end.year, + end.month, + end.day, + end.hour, + end.minute, + end.second, + end.microsecond, + tzinfo=end.tzinfo, + fold=end.fold, + ) + elif isinstance(end, pendulum.Date): + _end = date(end.year, end.month, end.day) + + # Fixing issues with datetime.__sub__() + # not handling offsets if the tzinfo is the same + if ( + isinstance(_start, datetime) + and isinstance(_end, datetime) + and _start.tzinfo is _end.tzinfo + ): + if _start.tzinfo is not None: + offset = cast(timedelta, cast(datetime, start).utcoffset()) + _start = (_start - offset).replace(tzinfo=None) + + if isinstance(end, datetime) and _end.tzinfo is not None: + offset = cast(timedelta, end.utcoffset()) + _end = (_end - offset).replace(tzinfo=None) + + delta: timedelta = _end - _start # type: ignore[operator] + + return cast(Interval, super().__new__(cls, seconds=delta.total_seconds())) + + def __init__( + self, + start: pendulum.DateTime | pendulum.Date | datetime | date, + end: pendulum.DateTime | pendulum.Date | datetime | date, + absolute: bool = False, + ) -> None: + super().__init__() + + _start: pendulum.DateTime | pendulum.Date | datetime | date + if not isinstance(start, pendulum.Date): + if isinstance(start, datetime): + start = pendulum.instance(start) + else: + start = pendulum.date(start.year, start.month, start.day) + + _start = start + else: + if isinstance(start, pendulum.DateTime): + _start = datetime( + start.year, + start.month, + start.day, + start.hour, + start.minute, + start.second, + start.microsecond, + tzinfo=start.tzinfo, + ) + else: + _start = date(start.year, start.month, start.day) + + _end: pendulum.DateTime | pendulum.Date | datetime | date + if not isinstance(end, pendulum.Date): + if isinstance(end, datetime): + end = pendulum.instance(end) + else: + end = pendulum.date(end.year, end.month, end.day) + + _end = end + else: + if isinstance(end, pendulum.DateTime): + _end = datetime( + end.year, + end.month, + end.day, + end.hour, + end.minute, + end.second, + end.microsecond, + tzinfo=end.tzinfo, + ) + else: + _end = date(end.year, end.month, end.day) + + self._invert = False + if start > end: + self._invert = True + + if absolute: + end, start = start, end + _end, _start = _start, _end + + self._absolute = absolute + self._start: pendulum.DateTime | pendulum.Date = start + self._end: pendulum.DateTime | pendulum.Date = end + self._delta: PreciseDiff = precise_diff(_start, _end) + + @property + def years(self) -> int: + return self._delta.years + + @property + def months(self) -> int: + return self._delta.months + + @property + def weeks(self) -> int: + return abs(self._delta.days) // 7 * self._sign(self._delta.days) + + @property + def days(self) -> int: + return self._days + + @property + def remaining_days(self) -> int: + return abs(self._delta.days) % 7 * self._sign(self._days) + + @property + def hours(self) -> int: + return self._delta.hours + + @property + def minutes(self) -> int: + return self._delta.minutes + + @property + def start(self) -> pendulum.DateTime | pendulum.Date | datetime | date: + return self._start + + @property + def end(self) -> pendulum.DateTime | pendulum.Date | datetime | date: + return self._end + + def in_years(self) -> int: + """ + Gives the duration of the Period in full years. + """ + return self.years + + def in_months(self) -> int: + """ + Gives the duration of the Period in full months. + """ + return self.years * MONTHS_PER_YEAR + self.months + + def in_weeks(self) -> int: + days = self.in_days() + sign = 1 + + if days < 0: + sign = -1 + + return sign * (abs(days) // 7) + + def in_days(self) -> int: + return self._delta.total_days + + def in_words(self, locale: str | None = None, separator: str = " ") -> str: + """ + Get the current interval in words in the current locale. + + Ex: 6 jours 23 heures 58 minutes + + :param locale: The locale to use. Defaults to current locale. + :param separator: The separator to use between each unit + """ + from pendulum.locales.locale import Locale # noqa + + periods = [ + ("year", self.years), + ("month", self.months), + ("week", self.weeks), + ("day", self.remaining_days), + ("hour", self.hours), + ("minute", self.minutes), + ("second", self.remaining_seconds), + ] + loaded_locale: Locale = Locale.load(locale or pendulum.get_locale()) + parts = [] + for period in periods: + unit, period_count = period + if abs(period_count) > 0: + translation = loaded_locale.translation( + f"units.{unit}.{loaded_locale.plural(abs(period_count))}" + ) + parts.append(translation.format(period_count)) + + if not parts: + count: str | int = 0 + if abs(self.microseconds) > 0: + unit = f"units.second.{loaded_locale.plural(1)}" + count = f"{abs(self.microseconds) / 1e6:.2f}" + else: + unit = f"units.microsecond.{loaded_locale.plural(0)}" + + translation = loaded_locale.translation(unit) + parts.append(translation.format(count)) + + return separator.join(parts) + + def range( + self, unit: str, amount: int = 1 + ) -> Iterator[pendulum.DateTime | pendulum.Date]: + method = "add" + op = operator.le + if not self._absolute and self.invert: + method = "subtract" + op = operator.ge + + start, end = self.start, self.end + + i = amount + while op(start, end): + yield cast(Union[pendulum.DateTime, pendulum.Date], start) + + start = getattr(self.start, method)(**{unit: i}) + + i += amount + + def as_interval(self) -> Duration: + """ + Return the Period as a Duration. + """ + return Duration(seconds=self.total_seconds()) + + def __iter__(self) -> Iterator[pendulum.DateTime | pendulum.Date]: + return self.range("days") + + def __contains__( + self, item: datetime | date | pendulum.DateTime | pendulum.Date + ) -> bool: + return self.start <= item <= self.end + + def __add__(self, other: timedelta) -> Duration: + return self.as_interval().__add__(other) + + __radd__ = __add__ + + def __sub__(self, other: timedelta) -> Duration: + return self.as_interval().__sub__(other) + + def __neg__(self) -> Interval: + return self.__class__(self.end, self.start, self._absolute) + + def __mul__(self, other: int | float) -> Duration: + return self.as_interval().__mul__(other) + + __rmul__ = __mul__ + + @overload + def __floordiv__(self, other: timedelta) -> int: + ... + + @overload + def __floordiv__(self, other: int) -> Duration: + ... + + def __floordiv__(self, other: int | timedelta) -> int | Duration: + return self.as_interval().__floordiv__(other) + + __div__ = __floordiv__ # type: ignore[assignment] + + @overload + def __truediv__(self, other: timedelta) -> float: + ... + + @overload + def __truediv__(self, other: float) -> Duration: + ... + + def __truediv__(self, other: float | timedelta) -> Duration | float: + return self.as_interval().__truediv__(other) + + def __mod__(self, other: timedelta) -> Duration: + return self.as_interval().__mod__(other) + + def __divmod__(self, other: timedelta) -> tuple[int, Duration]: + return self.as_interval().__divmod__(other) + + def __abs__(self) -> Interval: + return self.__class__(self.start, self.end, absolute=True) + + def __repr__(self) -> str: + return f"<Period [{self._start} -> {self._end}]>" + + def __str__(self) -> str: + return self.__repr__() + + def _cmp(self, other: timedelta) -> int: + # Only needed for PyPy + assert isinstance(other, timedelta) + + if isinstance(other, Interval): + other = other.as_timedelta() + + td = self.as_timedelta() + + return 0 if td == other else 1 if td > other else -1 + + def _getstate( + self, protocol: SupportsIndex = 3 + ) -> tuple[ + pendulum.DateTime | pendulum.Date | datetime | date, + pendulum.DateTime | pendulum.Date | datetime | date, + bool, + ]: + start, end = self.start, self.end + + if self._invert and self._absolute: + end, start = start, end + + return start, end, self._absolute + + def __reduce__( + self, + ) -> tuple[ + type[Interval], + tuple[ + pendulum.DateTime | pendulum.Date | datetime | date, + pendulum.DateTime | pendulum.Date | datetime | date, + bool, + ], + ]: + return self.__reduce_ex__(2) + + def __reduce_ex__( + self, protocol: SupportsIndex + ) -> tuple[ + type[Interval], + tuple[ + pendulum.DateTime | pendulum.Date | datetime | date, + pendulum.DateTime | pendulum.Date | datetime | date, + bool, + ], + ]: + return self.__class__, self._getstate(protocol) + + def __hash__(self) -> int: + return hash((self.start, self.end, self._absolute)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Interval): + return (self.start, self.end, self._absolute) == ( + other.start, + other.end, + other._absolute, + ) + else: + return self.as_interval() == other |