time.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. from __future__ import annotations
  2. import datetime
  3. from datetime import time
  4. from datetime import timedelta
  5. from typing import TYPE_CHECKING
  6. from typing import Optional
  7. from typing import cast
  8. from typing import overload
  9. import pendulum
  10. from pendulum.constants import SECS_PER_HOUR
  11. from pendulum.constants import SECS_PER_MIN
  12. from pendulum.constants import USECS_PER_SEC
  13. from pendulum.duration import AbsoluteDuration
  14. from pendulum.duration import Duration
  15. from pendulum.mixins.default import FormattableMixin
  16. from pendulum.tz.timezone import UTC
  17. if TYPE_CHECKING:
  18. from typing_extensions import Literal
  19. from typing_extensions import Self
  20. from typing_extensions import SupportsIndex
  21. from pendulum.tz.timezone import FixedTimezone
  22. from pendulum.tz.timezone import Timezone
  23. class Time(FormattableMixin, time):
  24. """
  25. Represents a time instance as hour, minute, second, microsecond.
  26. """
  27. @classmethod
  28. def instance(
  29. cls, t: time, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC
  30. ) -> Self:
  31. tz = t.tzinfo or tz
  32. if tz is not None:
  33. tz = pendulum._safe_timezone(tz)
  34. return cls(t.hour, t.minute, t.second, t.microsecond, tzinfo=tz, fold=t.fold)
  35. # String formatting
  36. def __repr__(self) -> str:
  37. us = ""
  38. if self.microsecond:
  39. us = f", {self.microsecond}"
  40. tzinfo = ""
  41. if self.tzinfo:
  42. tzinfo = f", tzinfo={self.tzinfo!r}"
  43. return (
  44. f"{self.__class__.__name__}"
  45. f"({self.hour}, {self.minute}, {self.second}{us}{tzinfo})"
  46. )
  47. # Comparisons
  48. def closest(self, dt1: Time | time, dt2: Time | time) -> Self:
  49. """
  50. Get the closest time from the instance.
  51. """
  52. dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
  53. dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)
  54. if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
  55. return dt1
  56. return dt2
  57. def farthest(self, dt1: Time | time, dt2: Time | time) -> Self:
  58. """
  59. Get the farthest time from the instance.
  60. """
  61. dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
  62. dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)
  63. if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
  64. return dt1
  65. return dt2
  66. # ADDITIONS AND SUBSTRACTIONS
  67. def add(
  68. self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0
  69. ) -> Time:
  70. """
  71. Add duration to the instance.
  72. :param hours: The number of hours
  73. :param minutes: The number of minutes
  74. :param seconds: The number of seconds
  75. :param microseconds: The number of microseconds
  76. """
  77. from pendulum.datetime import DateTime
  78. return (
  79. DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond)
  80. .add(
  81. hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds
  82. )
  83. .time()
  84. )
  85. def subtract(
  86. self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0
  87. ) -> Time:
  88. """
  89. Add duration to the instance.
  90. :param hours: The number of hours
  91. :type hours: int
  92. :param minutes: The number of minutes
  93. :type minutes: int
  94. :param seconds: The number of seconds
  95. :type seconds: int
  96. :param microseconds: The number of microseconds
  97. :type microseconds: int
  98. :rtype: Time
  99. """
  100. from pendulum.datetime import DateTime
  101. return (
  102. DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond)
  103. .subtract(
  104. hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds
  105. )
  106. .time()
  107. )
  108. def add_timedelta(self, delta: datetime.timedelta) -> Time:
  109. """
  110. Add timedelta duration to the instance.
  111. :param delta: The timedelta instance
  112. """
  113. if delta.days:
  114. raise TypeError("Cannot add timedelta with days to Time.")
  115. return self.add(seconds=delta.seconds, microseconds=delta.microseconds)
  116. def subtract_timedelta(self, delta: datetime.timedelta) -> Time:
  117. """
  118. Remove timedelta duration from the instance.
  119. :param delta: The timedelta instance
  120. """
  121. if delta.days:
  122. raise TypeError("Cannot subtract timedelta with days to Time.")
  123. return self.subtract(seconds=delta.seconds, microseconds=delta.microseconds)
  124. def __add__(self, other: datetime.timedelta) -> Time:
  125. if not isinstance(other, timedelta):
  126. return NotImplemented
  127. return self.add_timedelta(other)
  128. @overload
  129. def __sub__(self, other: time) -> pendulum.Duration:
  130. ...
  131. @overload
  132. def __sub__(self, other: datetime.timedelta) -> Time:
  133. ...
  134. def __sub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
  135. if not isinstance(other, (Time, time, timedelta)):
  136. return NotImplemented
  137. if isinstance(other, timedelta):
  138. return self.subtract_timedelta(other)
  139. if isinstance(other, time):
  140. if other.tzinfo is not None:
  141. raise TypeError("Cannot subtract aware times to or from Time.")
  142. other = self.__class__(
  143. other.hour, other.minute, other.second, other.microsecond
  144. )
  145. return other.diff(self, False)
  146. @overload
  147. def __rsub__(self, other: time) -> pendulum.Duration:
  148. ...
  149. @overload
  150. def __rsub__(self, other: datetime.timedelta) -> Time:
  151. ...
  152. def __rsub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
  153. if not isinstance(other, (Time, time)):
  154. return NotImplemented
  155. if isinstance(other, time):
  156. if other.tzinfo is not None:
  157. raise TypeError("Cannot subtract aware times to or from Time.")
  158. other = self.__class__(
  159. other.hour, other.minute, other.second, other.microsecond
  160. )
  161. return other.__sub__(self)
  162. # DIFFERENCES
  163. def diff(self, dt: time | None = None, abs: bool = True) -> Duration:
  164. """
  165. Returns the difference between two Time objects as an Duration.
  166. :param dt: The time to subtract from
  167. :param abs: Whether to return an absolute duration or not
  168. """
  169. if dt is None:
  170. dt = pendulum.now().time()
  171. else:
  172. dt = self.__class__(dt.hour, dt.minute, dt.second, dt.microsecond)
  173. us1 = (
  174. self.hour * SECS_PER_HOUR + self.minute * SECS_PER_MIN + self.second
  175. ) * USECS_PER_SEC
  176. us2 = (
  177. dt.hour * SECS_PER_HOUR + dt.minute * SECS_PER_MIN + dt.second
  178. ) * USECS_PER_SEC
  179. klass = Duration
  180. if abs:
  181. klass = AbsoluteDuration
  182. return klass(microseconds=us2 - us1)
  183. def diff_for_humans(
  184. self,
  185. other: time | None = None,
  186. absolute: bool = False,
  187. locale: str | None = None,
  188. ) -> str:
  189. """
  190. Get the difference in a human readable format in the current locale.
  191. :param dt: The time to subtract from
  192. :param absolute: removes time difference modifiers ago, after, etc
  193. :param locale: The locale to use for localization
  194. """
  195. is_now = other is None
  196. if is_now:
  197. other = pendulum.now().time()
  198. diff = self.diff(other)
  199. return pendulum.format_diff(diff, is_now, absolute, locale)
  200. # Compatibility methods
  201. def replace(
  202. self,
  203. hour: SupportsIndex | None = None,
  204. minute: SupportsIndex | None = None,
  205. second: SupportsIndex | None = None,
  206. microsecond: SupportsIndex | None = None,
  207. tzinfo: bool | datetime.tzinfo | Literal[True] | None = True,
  208. fold: int = 0,
  209. ) -> Self:
  210. if tzinfo is True:
  211. tzinfo = self.tzinfo
  212. hour = hour if hour is not None else self.hour
  213. minute = minute if minute is not None else self.minute
  214. second = second if second is not None else self.second
  215. microsecond = microsecond if microsecond is not None else self.microsecond
  216. t = super().replace(
  217. hour,
  218. minute,
  219. second,
  220. microsecond,
  221. tzinfo=cast(Optional[datetime.tzinfo], tzinfo),
  222. fold=fold,
  223. )
  224. return self.__class__(
  225. t.hour, t.minute, t.second, t.microsecond, tzinfo=t.tzinfo
  226. )
  227. def __getnewargs__(self) -> tuple[Time]:
  228. return (self,)
  229. def _get_state(
  230. self, protocol: SupportsIndex = 3
  231. ) -> tuple[int, int, int, int, datetime.tzinfo | None]:
  232. tz = self.tzinfo
  233. return self.hour, self.minute, self.second, self.microsecond, tz
  234. def __reduce__(
  235. self,
  236. ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]:
  237. return self.__reduce_ex__(2)
  238. def __reduce_ex__(
  239. self, protocol: SupportsIndex
  240. ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]:
  241. return self.__class__, self._get_state(protocol)
  242. Time.min = Time(0, 0, 0)
  243. Time.max = Time(23, 59, 59, 999999)
  244. Time.resolution = Duration(microseconds=1)