helpers.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. from __future__ import annotations
  2. import os
  3. import struct
  4. from datetime import date
  5. from datetime import datetime
  6. from datetime import timedelta
  7. from math import copysign
  8. from typing import TYPE_CHECKING
  9. from typing import TypeVar
  10. from typing import overload
  11. import pendulum
  12. from pendulum.constants import DAYS_PER_MONTHS
  13. from pendulum.day import WeekDay
  14. from pendulum.formatting.difference_formatter import DifferenceFormatter
  15. from pendulum.locales.locale import Locale
  16. if TYPE_CHECKING:
  17. # Prevent import cycles
  18. from pendulum.duration import Duration
  19. with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"
  20. _DT = TypeVar("_DT", bound=datetime)
  21. _D = TypeVar("_D", bound=date)
  22. try:
  23. if not with_extensions or struct.calcsize("P") == 4:
  24. raise ImportError()
  25. from pendulum._pendulum import PreciseDiff
  26. from pendulum._pendulum import days_in_year
  27. from pendulum._pendulum import is_leap
  28. from pendulum._pendulum import is_long_year
  29. from pendulum._pendulum import local_time
  30. from pendulum._pendulum import precise_diff
  31. from pendulum._pendulum import week_day
  32. except ImportError:
  33. from pendulum._helpers import PreciseDiff # type: ignore[assignment]
  34. from pendulum._helpers import days_in_year
  35. from pendulum._helpers import is_leap
  36. from pendulum._helpers import is_long_year
  37. from pendulum._helpers import local_time
  38. from pendulum._helpers import precise_diff # type: ignore[assignment]
  39. from pendulum._helpers import week_day
  40. difference_formatter = DifferenceFormatter()
  41. @overload
  42. def add_duration(
  43. dt: _DT,
  44. years: int = 0,
  45. months: int = 0,
  46. weeks: int = 0,
  47. days: int = 0,
  48. hours: int = 0,
  49. minutes: int = 0,
  50. seconds: float = 0,
  51. microseconds: int = 0,
  52. ) -> _DT:
  53. ...
  54. @overload
  55. def add_duration(
  56. dt: _D,
  57. years: int = 0,
  58. months: int = 0,
  59. weeks: int = 0,
  60. days: int = 0,
  61. ) -> _D:
  62. pass
  63. def add_duration(
  64. dt: date | datetime,
  65. years: int = 0,
  66. months: int = 0,
  67. weeks: int = 0,
  68. days: int = 0,
  69. hours: int = 0,
  70. minutes: int = 0,
  71. seconds: float = 0,
  72. microseconds: int = 0,
  73. ) -> date | datetime:
  74. """
  75. Adds a duration to a date/datetime instance.
  76. """
  77. days += weeks * 7
  78. if (
  79. isinstance(dt, date)
  80. and not isinstance(dt, datetime)
  81. and any([hours, minutes, seconds, microseconds])
  82. ):
  83. raise RuntimeError("Time elements cannot be added to a date instance.")
  84. # Normalizing
  85. if abs(microseconds) > 999999:
  86. s = _sign(microseconds)
  87. div, mod = divmod(microseconds * s, 1000000)
  88. microseconds = mod * s
  89. seconds += div * s
  90. if abs(seconds) > 59:
  91. s = _sign(seconds)
  92. div, mod = divmod(seconds * s, 60) # type: ignore[assignment]
  93. seconds = mod * s
  94. minutes += div * s
  95. if abs(minutes) > 59:
  96. s = _sign(minutes)
  97. div, mod = divmod(minutes * s, 60)
  98. minutes = mod * s
  99. hours += div * s
  100. if abs(hours) > 23:
  101. s = _sign(hours)
  102. div, mod = divmod(hours * s, 24)
  103. hours = mod * s
  104. days += div * s
  105. if abs(months) > 11:
  106. s = _sign(months)
  107. div, mod = divmod(months * s, 12)
  108. months = mod * s
  109. years += div * s
  110. year = dt.year + years
  111. month = dt.month
  112. if months:
  113. month += months
  114. if month > 12:
  115. year += 1
  116. month -= 12
  117. elif month < 1:
  118. year -= 1
  119. month += 12
  120. day = min(DAYS_PER_MONTHS[int(is_leap(year))][month], dt.day)
  121. dt = dt.replace(year=year, month=month, day=day)
  122. return dt + timedelta(
  123. days=days,
  124. hours=hours,
  125. minutes=minutes,
  126. seconds=seconds,
  127. microseconds=microseconds,
  128. )
  129. def format_diff(
  130. diff: Duration,
  131. is_now: bool = True,
  132. absolute: bool = False,
  133. locale: str | None = None,
  134. ) -> str:
  135. if locale is None:
  136. locale = get_locale()
  137. return difference_formatter.format(diff, is_now, absolute, locale)
  138. def _sign(x: float) -> int:
  139. return int(copysign(1, x))
  140. # Global helpers
  141. def locale(name: str) -> Locale:
  142. return Locale.load(name)
  143. def set_locale(name: str) -> None:
  144. locale(name)
  145. pendulum._LOCALE = name
  146. def get_locale() -> str:
  147. return pendulum._LOCALE
  148. def week_starts_at(wday: WeekDay) -> None:
  149. if wday < WeekDay.MONDAY or wday > WeekDay.SUNDAY:
  150. raise ValueError("Invalid day of week")
  151. pendulum._WEEK_STARTS_AT = wday
  152. def week_ends_at(wday: WeekDay) -> None:
  153. if wday < WeekDay.MONDAY or wday > WeekDay.SUNDAY:
  154. raise ValueError("Invalid day of week")
  155. pendulum._WEEK_ENDS_AT = wday
  156. __all__ = [
  157. "PreciseDiff",
  158. "days_in_year",
  159. "is_leap",
  160. "is_long_year",
  161. "local_time",
  162. "precise_diff",
  163. "week_day",
  164. "add_duration",
  165. "format_diff",
  166. "locale",
  167. "set_locale",
  168. "get_locale",
  169. "week_starts_at",
  170. "week_ends_at",
  171. ]