summaryrefslogtreecommitdiffstats
path: root/pendulum/interval.py
diff options
context:
space:
mode:
Diffstat (limited to 'pendulum/interval.py')
-rw-r--r--pendulum/interval.py448
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