datetime.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404
  1. from __future__ import annotations
  2. import calendar
  3. import datetime
  4. import traceback
  5. from typing import TYPE_CHECKING
  6. from typing import Any
  7. from typing import Callable
  8. from typing import ClassVar
  9. from typing import Optional
  10. from typing import cast
  11. from typing import overload
  12. import pendulum
  13. from pendulum.constants import ATOM
  14. from pendulum.constants import COOKIE
  15. from pendulum.constants import MINUTES_PER_HOUR
  16. from pendulum.constants import MONTHS_PER_YEAR
  17. from pendulum.constants import RFC822
  18. from pendulum.constants import RFC850
  19. from pendulum.constants import RFC1036
  20. from pendulum.constants import RFC1123
  21. from pendulum.constants import RFC2822
  22. from pendulum.constants import RSS
  23. from pendulum.constants import SECONDS_PER_DAY
  24. from pendulum.constants import SECONDS_PER_MINUTE
  25. from pendulum.constants import W3C
  26. from pendulum.constants import YEARS_PER_CENTURY
  27. from pendulum.constants import YEARS_PER_DECADE
  28. from pendulum.date import Date
  29. from pendulum.day import WeekDay
  30. from pendulum.exceptions import PendulumException
  31. from pendulum.helpers import add_duration
  32. from pendulum.interval import Interval
  33. from pendulum.time import Time
  34. from pendulum.tz import UTC
  35. from pendulum.tz import local_timezone
  36. from pendulum.tz.timezone import FixedTimezone
  37. from pendulum.tz.timezone import Timezone
  38. if TYPE_CHECKING:
  39. from typing_extensions import Literal
  40. from typing_extensions import Self
  41. from typing_extensions import SupportsIndex
  42. class DateTime(datetime.datetime, Date):
  43. EPOCH: ClassVar[DateTime]
  44. min: ClassVar[DateTime]
  45. max: ClassVar[DateTime]
  46. # Formats
  47. _FORMATS: ClassVar[dict[str, str | Callable[[datetime.datetime], str]]] = {
  48. "atom": ATOM,
  49. "cookie": COOKIE,
  50. "iso8601": lambda dt: dt.isoformat("T"),
  51. "rfc822": RFC822,
  52. "rfc850": RFC850,
  53. "rfc1036": RFC1036,
  54. "rfc1123": RFC1123,
  55. "rfc2822": RFC2822,
  56. "rfc3339": lambda dt: dt.isoformat("T"),
  57. "rss": RSS,
  58. "w3c": W3C,
  59. }
  60. _MODIFIERS_VALID_UNITS: ClassVar[list[str]] = [
  61. "second",
  62. "minute",
  63. "hour",
  64. "day",
  65. "week",
  66. "month",
  67. "year",
  68. "decade",
  69. "century",
  70. ]
  71. _EPOCH: datetime.datetime = datetime.datetime(1970, 1, 1, tzinfo=UTC)
  72. @classmethod
  73. def create(
  74. cls,
  75. year: SupportsIndex,
  76. month: SupportsIndex,
  77. day: SupportsIndex,
  78. hour: SupportsIndex = 0,
  79. minute: SupportsIndex = 0,
  80. second: SupportsIndex = 0,
  81. microsecond: SupportsIndex = 0,
  82. tz: str | float | Timezone | FixedTimezone | None | datetime.tzinfo = UTC,
  83. fold: int = 1,
  84. raise_on_unknown_times: bool = False,
  85. ) -> Self:
  86. """
  87. Creates a new DateTime instance from a specific date and time.
  88. """
  89. if tz is not None:
  90. tz = pendulum._safe_timezone(tz)
  91. dt = datetime.datetime(
  92. year, month, day, hour, minute, second, microsecond, fold=fold
  93. )
  94. if tz is not None:
  95. dt = tz.convert(dt, raise_on_unknown_times=raise_on_unknown_times)
  96. return cls(
  97. dt.year,
  98. dt.month,
  99. dt.day,
  100. dt.hour,
  101. dt.minute,
  102. dt.second,
  103. dt.microsecond,
  104. tzinfo=dt.tzinfo,
  105. fold=dt.fold,
  106. )
  107. @classmethod
  108. def instance(
  109. cls,
  110. dt: datetime.datetime,
  111. tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC,
  112. ) -> Self:
  113. tz = dt.tzinfo or tz
  114. if tz is not None:
  115. tz = pendulum._safe_timezone(tz, dt=dt)
  116. return cls.create(
  117. dt.year,
  118. dt.month,
  119. dt.day,
  120. dt.hour,
  121. dt.minute,
  122. dt.second,
  123. dt.microsecond,
  124. tz=tz,
  125. fold=dt.fold,
  126. )
  127. @overload
  128. @classmethod
  129. def now(cls, tz: datetime.tzinfo | None = None) -> Self:
  130. ...
  131. @overload
  132. @classmethod
  133. def now(cls, tz: str | Timezone | FixedTimezone | None = None) -> Self:
  134. ...
  135. @classmethod
  136. def now(
  137. cls, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None
  138. ) -> Self:
  139. """
  140. Get a DateTime instance for the current date and time.
  141. """
  142. if tz is None or tz == "local":
  143. dt = datetime.datetime.now(local_timezone())
  144. elif tz is UTC or tz == "UTC":
  145. dt = datetime.datetime.now(UTC)
  146. else:
  147. dt = datetime.datetime.now(UTC)
  148. tz = pendulum._safe_timezone(tz)
  149. dt = dt.astimezone(tz)
  150. return cls(
  151. dt.year,
  152. dt.month,
  153. dt.day,
  154. dt.hour,
  155. dt.minute,
  156. dt.second,
  157. dt.microsecond,
  158. tzinfo=dt.tzinfo,
  159. fold=dt.fold,
  160. )
  161. @classmethod
  162. def utcnow(cls) -> Self:
  163. """
  164. Get a DateTime instance for the current date and time in UTC.
  165. """
  166. return cls.now(UTC)
  167. @classmethod
  168. def today(cls) -> Self:
  169. return cls.now()
  170. @classmethod
  171. def strptime(cls, time: str, fmt: str) -> Self:
  172. return cls.instance(datetime.datetime.strptime(time, fmt))
  173. # Getters/Setters
  174. def set(
  175. self,
  176. year: int | None = None,
  177. month: int | None = None,
  178. day: int | None = None,
  179. hour: int | None = None,
  180. minute: int | None = None,
  181. second: int | None = None,
  182. microsecond: int | None = None,
  183. tz: str | float | Timezone | FixedTimezone | datetime.tzinfo | None = None,
  184. ) -> Self:
  185. if year is None:
  186. year = self.year
  187. if month is None:
  188. month = self.month
  189. if day is None:
  190. day = self.day
  191. if hour is None:
  192. hour = self.hour
  193. if minute is None:
  194. minute = self.minute
  195. if second is None:
  196. second = self.second
  197. if microsecond is None:
  198. microsecond = self.microsecond
  199. if tz is None:
  200. tz = self.tz
  201. return self.__class__.create(
  202. year, month, day, hour, minute, second, microsecond, tz=tz, fold=self.fold
  203. )
  204. @property
  205. def float_timestamp(self) -> float:
  206. return self.timestamp()
  207. @property
  208. def int_timestamp(self) -> int:
  209. # Workaround needed to avoid inaccuracy
  210. # for far into the future datetimes
  211. dt = datetime.datetime(
  212. self.year,
  213. self.month,
  214. self.day,
  215. self.hour,
  216. self.minute,
  217. self.second,
  218. self.microsecond,
  219. tzinfo=self.tzinfo,
  220. fold=self.fold,
  221. )
  222. delta = dt - self._EPOCH
  223. return delta.days * SECONDS_PER_DAY + delta.seconds
  224. @property
  225. def offset(self) -> int | None:
  226. return self.get_offset()
  227. @property
  228. def offset_hours(self) -> float | None:
  229. offset = self.get_offset()
  230. if offset is None:
  231. return None
  232. return offset / SECONDS_PER_MINUTE / MINUTES_PER_HOUR
  233. @property
  234. def timezone(self) -> Timezone | FixedTimezone | None:
  235. if not isinstance(self.tzinfo, (Timezone, FixedTimezone)):
  236. return None
  237. return self.tzinfo
  238. @property
  239. def tz(self) -> Timezone | FixedTimezone | None:
  240. return self.timezone
  241. @property
  242. def timezone_name(self) -> str | None:
  243. tz = self.timezone
  244. if tz is None:
  245. return None
  246. return tz.name
  247. @property
  248. def age(self) -> int:
  249. return self.date().diff(self.now(self.tz).date(), abs=False).in_years()
  250. def is_local(self) -> bool:
  251. return self.offset == self.in_timezone(pendulum.local_timezone()).offset
  252. def is_utc(self) -> bool:
  253. return self.offset == 0
  254. def is_dst(self) -> bool:
  255. return self.dst() != datetime.timedelta()
  256. def get_offset(self) -> int | None:
  257. utcoffset = self.utcoffset()
  258. if utcoffset is None:
  259. return None
  260. return int(utcoffset.total_seconds())
  261. def date(self) -> Date:
  262. return Date(self.year, self.month, self.day)
  263. def time(self) -> Time:
  264. return Time(self.hour, self.minute, self.second, self.microsecond)
  265. def naive(self) -> Self:
  266. """
  267. Return the DateTime without timezone information.
  268. """
  269. return self.__class__(
  270. self.year,
  271. self.month,
  272. self.day,
  273. self.hour,
  274. self.minute,
  275. self.second,
  276. self.microsecond,
  277. )
  278. def on(self, year: int, month: int, day: int) -> Self:
  279. """
  280. Returns a new instance with the current date set to a different date.
  281. """
  282. return self.set(year=int(year), month=int(month), day=int(day))
  283. def at(
  284. self, hour: int, minute: int = 0, second: int = 0, microsecond: int = 0
  285. ) -> Self:
  286. """
  287. Returns a new instance with the current time to a different time.
  288. """
  289. return self.set(
  290. hour=hour, minute=minute, second=second, microsecond=microsecond
  291. )
  292. def in_timezone(self, tz: str | Timezone | FixedTimezone) -> Self:
  293. """
  294. Set the instance's timezone from a string or object.
  295. """
  296. tz = pendulum._safe_timezone(tz)
  297. dt = self
  298. if not self.timezone:
  299. dt = dt.replace(fold=1)
  300. return tz.convert(dt)
  301. def in_tz(self, tz: str | Timezone | FixedTimezone) -> Self:
  302. """
  303. Set the instance's timezone from a string or object.
  304. """
  305. return self.in_timezone(tz)
  306. # STRING FORMATTING
  307. def to_time_string(self) -> str:
  308. """
  309. Format the instance as time.
  310. """
  311. return self.format("HH:mm:ss")
  312. def to_datetime_string(self) -> str:
  313. """
  314. Format the instance as date and time.
  315. """
  316. return self.format("YYYY-MM-DD HH:mm:ss")
  317. def to_day_datetime_string(self) -> str:
  318. """
  319. Format the instance as day, date and time (in english).
  320. """
  321. return self.format("ddd, MMM D, YYYY h:mm A", locale="en")
  322. def to_atom_string(self) -> str:
  323. """
  324. Format the instance as ATOM.
  325. """
  326. return self._to_string("atom")
  327. def to_cookie_string(self) -> str:
  328. """
  329. Format the instance as COOKIE.
  330. """
  331. return self._to_string("cookie", locale="en")
  332. def to_iso8601_string(self) -> str:
  333. """
  334. Format the instance as ISO 8601.
  335. """
  336. string = self._to_string("iso8601")
  337. if self.tz and self.tz.name == "UTC":
  338. string = string.replace("+00:00", "Z")
  339. return string
  340. def to_rfc822_string(self) -> str:
  341. """
  342. Format the instance as RFC 822.
  343. """
  344. return self._to_string("rfc822")
  345. def to_rfc850_string(self) -> str:
  346. """
  347. Format the instance as RFC 850.
  348. """
  349. return self._to_string("rfc850")
  350. def to_rfc1036_string(self) -> str:
  351. """
  352. Format the instance as RFC 1036.
  353. """
  354. return self._to_string("rfc1036")
  355. def to_rfc1123_string(self) -> str:
  356. """
  357. Format the instance as RFC 1123.
  358. """
  359. return self._to_string("rfc1123")
  360. def to_rfc2822_string(self) -> str:
  361. """
  362. Format the instance as RFC 2822.
  363. """
  364. return self._to_string("rfc2822")
  365. def to_rfc3339_string(self) -> str:
  366. """
  367. Format the instance as RFC 3339.
  368. """
  369. return self._to_string("rfc3339")
  370. def to_rss_string(self) -> str:
  371. """
  372. Format the instance as RSS.
  373. """
  374. return self._to_string("rss")
  375. def to_w3c_string(self) -> str:
  376. """
  377. Format the instance as W3C.
  378. """
  379. return self._to_string("w3c")
  380. def _to_string(self, fmt: str, locale: str | None = None) -> str:
  381. """
  382. Format the instance to a common string format.
  383. """
  384. if fmt not in self._FORMATS:
  385. raise ValueError(f"Format [{fmt}] is not supported")
  386. fmt_value = self._FORMATS[fmt]
  387. if callable(fmt_value):
  388. return fmt_value(self)
  389. return self.format(fmt_value, locale=locale)
  390. def __str__(self) -> str:
  391. return self.isoformat(" ")
  392. def __repr__(self) -> str:
  393. us = ""
  394. if self.microsecond:
  395. us = f", {self.microsecond}"
  396. repr_ = "{klass}(" "{year}, {month}, {day}, " "{hour}, {minute}, {second}{us}"
  397. if self.tzinfo is not None:
  398. repr_ += ", tzinfo={tzinfo}"
  399. repr_ += ")"
  400. return repr_.format(
  401. klass=self.__class__.__name__,
  402. year=self.year,
  403. month=self.month,
  404. day=self.day,
  405. hour=self.hour,
  406. minute=self.minute,
  407. second=self.second,
  408. us=us,
  409. tzinfo=repr(self.tzinfo),
  410. )
  411. # Comparisons
  412. def closest(self, *dts: datetime.datetime) -> Self: # type: ignore[override]
  413. """
  414. Get the closest date to the instance.
  415. """
  416. pdts = [self.instance(x) for x in dts]
  417. return min((abs(self - dt), dt) for dt in pdts)[1]
  418. def farthest(self, *dts: datetime.datetime) -> Self: # type: ignore[override]
  419. """
  420. Get the farthest date from the instance.
  421. """
  422. pdts = [self.instance(x) for x in dts]
  423. return max((abs(self - dt), dt) for dt in pdts)[1]
  424. def is_future(self) -> bool:
  425. """
  426. Determines if the instance is in the future, ie. greater than now.
  427. """
  428. return self > self.now(self.timezone)
  429. def is_past(self) -> bool:
  430. """
  431. Determines if the instance is in the past, ie. less than now.
  432. """
  433. return self < self.now(self.timezone)
  434. def is_long_year(self) -> bool:
  435. """
  436. Determines if the instance is a long year
  437. See link `https://en.wikipedia.org/wiki/ISO_8601#Week_dates`_
  438. """
  439. return (
  440. DateTime.create(self.year, 12, 28, 0, 0, 0, tz=self.tz).isocalendar()[1]
  441. == 53
  442. )
  443. def is_same_day(self, dt: datetime.datetime) -> bool: # type: ignore[override]
  444. """
  445. Checks if the passed in date is the same day
  446. as the instance current day.
  447. """
  448. dt = self.instance(dt)
  449. return self.to_date_string() == dt.to_date_string()
  450. def is_anniversary( # type: ignore[override]
  451. self, dt: datetime.datetime | None = None
  452. ) -> bool:
  453. """
  454. Check if its the anniversary.
  455. Compares the date/month values of the two dates.
  456. """
  457. if dt is None:
  458. dt = self.now(self.tz)
  459. instance = self.instance(dt)
  460. return (self.month, self.day) == (instance.month, instance.day)
  461. # ADDITIONS AND SUBSTRACTIONS
  462. def add(
  463. self,
  464. years: int = 0,
  465. months: int = 0,
  466. weeks: int = 0,
  467. days: int = 0,
  468. hours: int = 0,
  469. minutes: int = 0,
  470. seconds: float = 0,
  471. microseconds: int = 0,
  472. ) -> Self:
  473. """
  474. Add a duration to the instance.
  475. If we're adding units of variable length (i.e., years, months),
  476. move forward from current time, otherwise move forward from utc, for accuracy
  477. when moving across DST boundaries.
  478. """
  479. units_of_variable_length = any([years, months, weeks, days])
  480. current_dt = datetime.datetime(
  481. self.year,
  482. self.month,
  483. self.day,
  484. self.hour,
  485. self.minute,
  486. self.second,
  487. self.microsecond,
  488. )
  489. if not units_of_variable_length:
  490. offset = self.utcoffset()
  491. if offset:
  492. current_dt = current_dt - offset
  493. dt = add_duration(
  494. current_dt,
  495. years=years,
  496. months=months,
  497. weeks=weeks,
  498. days=days,
  499. hours=hours,
  500. minutes=minutes,
  501. seconds=seconds,
  502. microseconds=microseconds,
  503. )
  504. if units_of_variable_length or self.tz is None:
  505. return self.__class__.create(
  506. dt.year,
  507. dt.month,
  508. dt.day,
  509. dt.hour,
  510. dt.minute,
  511. dt.second,
  512. dt.microsecond,
  513. tz=self.tz,
  514. )
  515. dt = datetime.datetime(
  516. dt.year,
  517. dt.month,
  518. dt.day,
  519. dt.hour,
  520. dt.minute,
  521. dt.second,
  522. dt.microsecond,
  523. tzinfo=UTC,
  524. )
  525. dt = self.tz.convert(dt)
  526. return self.__class__(
  527. dt.year,
  528. dt.month,
  529. dt.day,
  530. dt.hour,
  531. dt.minute,
  532. dt.second,
  533. dt.microsecond,
  534. tzinfo=self.tz,
  535. fold=dt.fold,
  536. )
  537. def subtract(
  538. self,
  539. years: int = 0,
  540. months: int = 0,
  541. weeks: int = 0,
  542. days: int = 0,
  543. hours: int = 0,
  544. minutes: int = 0,
  545. seconds: float = 0,
  546. microseconds: int = 0,
  547. ) -> Self:
  548. """
  549. Remove duration from the instance.
  550. """
  551. return self.add(
  552. years=-years,
  553. months=-months,
  554. weeks=-weeks,
  555. days=-days,
  556. hours=-hours,
  557. minutes=-minutes,
  558. seconds=-seconds,
  559. microseconds=-microseconds,
  560. )
  561. # Adding a final underscore to the method name
  562. # to avoid errors for PyPy which already defines
  563. # a _add_timedelta method
  564. def _add_timedelta_(self, delta: datetime.timedelta) -> Self:
  565. """
  566. Add timedelta duration to the instance.
  567. """
  568. if isinstance(delta, pendulum.Interval):
  569. return self.add(
  570. years=delta.years,
  571. months=delta.months,
  572. weeks=delta.weeks,
  573. days=delta.remaining_days,
  574. hours=delta.hours,
  575. minutes=delta.minutes,
  576. seconds=delta.remaining_seconds,
  577. microseconds=delta.microseconds,
  578. )
  579. elif isinstance(delta, pendulum.Duration):
  580. return self.add(**delta._signature) # type: ignore[attr-defined]
  581. return self.add(seconds=delta.total_seconds())
  582. def _subtract_timedelta(self, delta: datetime.timedelta) -> Self:
  583. """
  584. Remove timedelta duration from the instance.
  585. """
  586. if isinstance(delta, pendulum.Duration):
  587. return self.subtract(
  588. years=delta.years, months=delta.months, seconds=delta._total
  589. )
  590. return self.subtract(seconds=delta.total_seconds())
  591. # DIFFERENCES
  592. def diff( # type: ignore[override]
  593. self, dt: datetime.datetime | None = None, abs: bool = True
  594. ) -> Interval:
  595. """
  596. Returns the difference between two DateTime objects represented as an Interval.
  597. """
  598. if dt is None:
  599. dt = self.now(self.tz)
  600. return Interval(self, dt, absolute=abs)
  601. def diff_for_humans( # type: ignore[override]
  602. self,
  603. other: DateTime | None = None,
  604. absolute: bool = False,
  605. locale: str | None = None,
  606. ) -> str:
  607. """
  608. Get the difference in a human readable format in the current locale.
  609. When comparing a value in the past to default now:
  610. 1 day ago
  611. 5 months ago
  612. When comparing a value in the future to default now:
  613. 1 day from now
  614. 5 months from now
  615. When comparing a value in the past to another value:
  616. 1 day before
  617. 5 months before
  618. When comparing a value in the future to another value:
  619. 1 day after
  620. 5 months after
  621. """
  622. is_now = other is None
  623. if is_now:
  624. other = self.now()
  625. diff = self.diff(other)
  626. return pendulum.format_diff(diff, is_now, absolute, locale)
  627. # Modifiers
  628. def start_of(self, unit: str) -> Self:
  629. """
  630. Returns a copy of the instance with the time reset
  631. with the following rules:
  632. * second: microsecond set to 0
  633. * minute: second and microsecond set to 0
  634. * hour: minute, second and microsecond set to 0
  635. * day: time to 00:00:00
  636. * week: date to first day of the week and time to 00:00:00
  637. * month: date to first day of the month and time to 00:00:00
  638. * year: date to first day of the year and time to 00:00:00
  639. * decade: date to first day of the decade and time to 00:00:00
  640. * century: date to first day of century and time to 00:00:00
  641. """
  642. if unit not in self._MODIFIERS_VALID_UNITS:
  643. raise ValueError(f'Invalid unit "{unit}" for start_of()')
  644. return cast("Self", getattr(self, f"_start_of_{unit}")())
  645. def end_of(self, unit: str) -> Self:
  646. """
  647. Returns a copy of the instance with the time reset
  648. with the following rules:
  649. * second: microsecond set to 999999
  650. * minute: second set to 59 and microsecond set to 999999
  651. * hour: minute and second set to 59 and microsecond set to 999999
  652. * day: time to 23:59:59.999999
  653. * week: date to last day of the week and time to 23:59:59.999999
  654. * month: date to last day of the month and time to 23:59:59.999999
  655. * year: date to last day of the year and time to 23:59:59.999999
  656. * decade: date to last day of the decade and time to 23:59:59.999999
  657. * century: date to last day of century and time to 23:59:59.999999
  658. """
  659. if unit not in self._MODIFIERS_VALID_UNITS:
  660. raise ValueError(f'Invalid unit "{unit}" for end_of()')
  661. return cast("Self", getattr(self, f"_end_of_{unit}")())
  662. def _start_of_second(self) -> Self:
  663. """
  664. Reset microseconds to 0.
  665. """
  666. return self.set(microsecond=0)
  667. def _end_of_second(self) -> Self:
  668. """
  669. Set microseconds to 999999.
  670. """
  671. return self.set(microsecond=999999)
  672. def _start_of_minute(self) -> Self:
  673. """
  674. Reset seconds and microseconds to 0.
  675. """
  676. return self.set(second=0, microsecond=0)
  677. def _end_of_minute(self) -> Self:
  678. """
  679. Set seconds to 59 and microseconds to 999999.
  680. """
  681. return self.set(second=59, microsecond=999999)
  682. def _start_of_hour(self) -> Self:
  683. """
  684. Reset minutes, seconds and microseconds to 0.
  685. """
  686. return self.set(minute=0, second=0, microsecond=0)
  687. def _end_of_hour(self) -> Self:
  688. """
  689. Set minutes and seconds to 59 and microseconds to 999999.
  690. """
  691. return self.set(minute=59, second=59, microsecond=999999)
  692. def _start_of_day(self) -> Self:
  693. """
  694. Reset the time to 00:00:00.
  695. """
  696. return self.at(0, 0, 0, 0)
  697. def _end_of_day(self) -> Self:
  698. """
  699. Reset the time to 23:59:59.999999.
  700. """
  701. return self.at(23, 59, 59, 999999)
  702. def _start_of_month(self) -> Self:
  703. """
  704. Reset the date to the first day of the month and the time to 00:00:00.
  705. """
  706. return self.set(self.year, self.month, 1, 0, 0, 0, 0)
  707. def _end_of_month(self) -> Self:
  708. """
  709. Reset the date to the last day of the month
  710. and the time to 23:59:59.999999.
  711. """
  712. return self.set(self.year, self.month, self.days_in_month, 23, 59, 59, 999999)
  713. def _start_of_year(self) -> Self:
  714. """
  715. Reset the date to the first day of the year and the time to 00:00:00.
  716. """
  717. return self.set(self.year, 1, 1, 0, 0, 0, 0)
  718. def _end_of_year(self) -> Self:
  719. """
  720. Reset the date to the last day of the year
  721. and the time to 23:59:59.999999.
  722. """
  723. return self.set(self.year, 12, 31, 23, 59, 59, 999999)
  724. def _start_of_decade(self) -> Self:
  725. """
  726. Reset the date to the first day of the decade
  727. and the time to 00:00:00.
  728. """
  729. year = self.year - self.year % YEARS_PER_DECADE
  730. return self.set(year, 1, 1, 0, 0, 0, 0)
  731. def _end_of_decade(self) -> Self:
  732. """
  733. Reset the date to the last day of the decade
  734. and the time to 23:59:59.999999.
  735. """
  736. year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
  737. return self.set(year, 12, 31, 23, 59, 59, 999999)
  738. def _start_of_century(self) -> Self:
  739. """
  740. Reset the date to the first day of the century
  741. and the time to 00:00:00.
  742. """
  743. year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
  744. return self.set(year, 1, 1, 0, 0, 0, 0)
  745. def _end_of_century(self) -> Self:
  746. """
  747. Reset the date to the last day of the century
  748. and the time to 23:59:59.999999.
  749. """
  750. year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
  751. return self.set(year, 12, 31, 23, 59, 59, 999999)
  752. def _start_of_week(self) -> Self:
  753. """
  754. Reset the date to the first day of the week
  755. and the time to 00:00:00.
  756. """
  757. dt = self
  758. if self.day_of_week != pendulum._WEEK_STARTS_AT:
  759. dt = self.previous(pendulum._WEEK_STARTS_AT)
  760. return dt.start_of("day")
  761. def _end_of_week(self) -> Self:
  762. """
  763. Reset the date to the last day of the week
  764. and the time to 23:59:59.
  765. """
  766. dt = self
  767. if self.day_of_week != pendulum._WEEK_ENDS_AT:
  768. dt = self.next(pendulum._WEEK_ENDS_AT)
  769. return dt.end_of("day")
  770. def next(self, day_of_week: WeekDay | None = None, keep_time: bool = False) -> Self:
  771. """
  772. Modify to the next occurrence of a given day of the week.
  773. If no day_of_week is provided, modify to the next occurrence
  774. of the current day of the week. Use the supplied consts
  775. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  776. """
  777. if day_of_week is None:
  778. day_of_week = self.day_of_week
  779. if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY:
  780. raise ValueError("Invalid day of week")
  781. dt = self if keep_time else self.start_of("day")
  782. dt = dt.add(days=1)
  783. while dt.day_of_week != day_of_week:
  784. dt = dt.add(days=1)
  785. return dt
  786. def previous(
  787. self, day_of_week: WeekDay | None = None, keep_time: bool = False
  788. ) -> Self:
  789. """
  790. Modify to the previous occurrence of a given day of the week.
  791. If no day_of_week is provided, modify to the previous occurrence
  792. of the current day of the week. Use the supplied consts
  793. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  794. """
  795. if day_of_week is None:
  796. day_of_week = self.day_of_week
  797. if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY:
  798. raise ValueError("Invalid day of week")
  799. dt = self if keep_time else self.start_of("day")
  800. dt = dt.subtract(days=1)
  801. while dt.day_of_week != day_of_week:
  802. dt = dt.subtract(days=1)
  803. return dt
  804. def first_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self:
  805. """
  806. Returns an instance set to the first occurrence
  807. of a given day of the week in the current unit.
  808. If no day_of_week is provided, modify to the first day of the unit.
  809. Use the supplied consts to indicate the desired day_of_week,
  810. ex. DateTime.MONDAY.
  811. Supported units are month, quarter and year.
  812. """
  813. if unit not in ["month", "quarter", "year"]:
  814. raise ValueError(f'Invalid unit "{unit}" for first_of()')
  815. return cast("Self", getattr(self, f"_first_of_{unit}")(day_of_week))
  816. def last_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self:
  817. """
  818. Returns an instance set to the last occurrence
  819. of a given day of the week in the current unit.
  820. If no day_of_week is provided, modify to the last day of the unit.
  821. Use the supplied consts to indicate the desired day_of_week,
  822. ex. DateTime.MONDAY.
  823. Supported units are month, quarter and year.
  824. """
  825. if unit not in ["month", "quarter", "year"]:
  826. raise ValueError(f'Invalid unit "{unit}" for first_of()')
  827. return cast("Self", getattr(self, f"_last_of_{unit}")(day_of_week))
  828. def nth_of(self, unit: str, nth: int, day_of_week: WeekDay) -> Self:
  829. """
  830. Returns a new instance set to the given occurrence
  831. of a given day of the week in the current unit.
  832. If the calculated occurrence is outside the scope of the current unit,
  833. then raise an error. Use the supplied consts
  834. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  835. Supported units are month, quarter and year.
  836. """
  837. if unit not in ["month", "quarter", "year"]:
  838. raise ValueError(f'Invalid unit "{unit}" for first_of()')
  839. dt = cast(Optional["Self"], getattr(self, f"_nth_of_{unit}")(nth, day_of_week))
  840. if not dt:
  841. raise PendulumException(
  842. f"Unable to find occurrence {nth}"
  843. f" of {WeekDay(day_of_week).name.capitalize()} in {unit}"
  844. )
  845. return dt
  846. def _first_of_month(self, day_of_week: WeekDay | None = None) -> Self:
  847. """
  848. Modify to the first occurrence of a given day of the week
  849. in the current month. If no day_of_week is provided,
  850. modify to the first day of the month. Use the supplied consts
  851. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  852. """
  853. dt = self.start_of("day")
  854. if day_of_week is None:
  855. return dt.set(day=1)
  856. month = calendar.monthcalendar(dt.year, dt.month)
  857. calendar_day = day_of_week
  858. if month[0][calendar_day] > 0:
  859. day_of_month = month[0][calendar_day]
  860. else:
  861. day_of_month = month[1][calendar_day]
  862. return dt.set(day=day_of_month)
  863. def _last_of_month(self, day_of_week: WeekDay | None = None) -> Self:
  864. """
  865. Modify to the last occurrence of a given day of the week
  866. in the current month. If no day_of_week is provided,
  867. modify to the last day of the month. Use the supplied consts
  868. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  869. """
  870. dt = self.start_of("day")
  871. if day_of_week is None:
  872. return dt.set(day=self.days_in_month)
  873. month = calendar.monthcalendar(dt.year, dt.month)
  874. calendar_day = day_of_week
  875. if month[-1][calendar_day] > 0:
  876. day_of_month = month[-1][calendar_day]
  877. else:
  878. day_of_month = month[-2][calendar_day]
  879. return dt.set(day=day_of_month)
  880. def _nth_of_month(
  881. self, nth: int, day_of_week: WeekDay | None = None
  882. ) -> Self | None:
  883. """
  884. Modify to the given occurrence of a given day of the week
  885. in the current month. If the calculated occurrence is outside,
  886. the scope of the current month, then return False and no
  887. modifications are made. Use the supplied consts
  888. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  889. """
  890. if nth == 1:
  891. return self.first_of("month", day_of_week)
  892. dt = self.first_of("month")
  893. check = dt.format("%Y-%M")
  894. for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
  895. dt = dt.next(day_of_week)
  896. if dt.format("%Y-%M") == check:
  897. return self.set(day=dt.day).start_of("day")
  898. return None
  899. def _first_of_quarter(self, day_of_week: WeekDay | None = None) -> Self:
  900. """
  901. Modify to the first occurrence of a given day of the week
  902. in the current quarter. If no day_of_week is provided,
  903. modify to the first day of the quarter. Use the supplied consts
  904. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  905. """
  906. return self.on(self.year, self.quarter * 3 - 2, 1).first_of(
  907. "month", day_of_week
  908. )
  909. def _last_of_quarter(self, day_of_week: WeekDay | None = None) -> Self:
  910. """
  911. Modify to the last occurrence of a given day of the week
  912. in the current quarter. If no day_of_week is provided,
  913. modify to the last day of the quarter. Use the supplied consts
  914. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  915. """
  916. return self.on(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
  917. def _nth_of_quarter(
  918. self, nth: int, day_of_week: WeekDay | None = None
  919. ) -> Self | None:
  920. """
  921. Modify to the given occurrence of a given day of the week
  922. in the current quarter. If the calculated occurrence is outside,
  923. the scope of the current quarter, then return False and no
  924. modifications are made. Use the supplied consts
  925. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  926. """
  927. if nth == 1:
  928. return self.first_of("quarter", day_of_week)
  929. dt = self.set(day=1, month=self.quarter * 3)
  930. last_month = dt.month
  931. year = dt.year
  932. dt = dt.first_of("quarter")
  933. for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
  934. dt = dt.next(day_of_week)
  935. if last_month < dt.month or year != dt.year:
  936. return None
  937. return self.on(self.year, dt.month, dt.day).start_of("day")
  938. def _first_of_year(self, day_of_week: WeekDay | None = None) -> Self:
  939. """
  940. Modify to the first occurrence of a given day of the week
  941. in the current year. If no day_of_week is provided,
  942. modify to the first day of the year. Use the supplied consts
  943. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  944. """
  945. return self.set(month=1).first_of("month", day_of_week)
  946. def _last_of_year(self, day_of_week: WeekDay | None = None) -> Self:
  947. """
  948. Modify to the last occurrence of a given day of the week
  949. in the current year. If no day_of_week is provided,
  950. modify to the last day of the year. Use the supplied consts
  951. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  952. """
  953. return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
  954. def _nth_of_year(self, nth: int, day_of_week: WeekDay | None = None) -> Self | None:
  955. """
  956. Modify to the given occurrence of a given day of the week
  957. in the current year. If the calculated occurrence is outside,
  958. the scope of the current year, then return False and no
  959. modifications are made. Use the supplied consts
  960. to indicate the desired day_of_week, ex. DateTime.MONDAY.
  961. """
  962. if nth == 1:
  963. return self.first_of("year", day_of_week)
  964. dt = self.first_of("year")
  965. year = dt.year
  966. for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
  967. dt = dt.next(day_of_week)
  968. if year != dt.year:
  969. return None
  970. return self.on(self.year, dt.month, dt.day).start_of("day")
  971. def average( # type: ignore[override]
  972. self, dt: datetime.datetime | None = None
  973. ) -> Self:
  974. """
  975. Modify the current instance to the average
  976. of a given instance (default now) and the current instance.
  977. """
  978. if dt is None:
  979. dt = self.now(self.tz)
  980. diff = self.diff(dt, False)
  981. return self.add(
  982. microseconds=(diff.in_seconds() * 1000000 + diff.microseconds) // 2
  983. )
  984. @overload # type: ignore[override]
  985. def __sub__(self, other: datetime.timedelta) -> Self:
  986. ...
  987. @overload
  988. def __sub__(self, other: DateTime) -> Interval:
  989. ...
  990. def __sub__(self, other: datetime.datetime | datetime.timedelta) -> Self | Interval:
  991. if isinstance(other, datetime.timedelta):
  992. return self._subtract_timedelta(other)
  993. if not isinstance(other, datetime.datetime):
  994. return NotImplemented
  995. if not isinstance(other, self.__class__):
  996. if other.tzinfo is None:
  997. other = pendulum.naive(
  998. other.year,
  999. other.month,
  1000. other.day,
  1001. other.hour,
  1002. other.minute,
  1003. other.second,
  1004. other.microsecond,
  1005. )
  1006. else:
  1007. other = self.instance(other)
  1008. return other.diff(self, False)
  1009. def __rsub__(self, other: datetime.datetime) -> Interval:
  1010. if not isinstance(other, datetime.datetime):
  1011. return NotImplemented
  1012. if not isinstance(other, self.__class__):
  1013. if other.tzinfo is None:
  1014. other = pendulum.naive(
  1015. other.year,
  1016. other.month,
  1017. other.day,
  1018. other.hour,
  1019. other.minute,
  1020. other.second,
  1021. other.microsecond,
  1022. )
  1023. else:
  1024. other = self.instance(other)
  1025. return self.diff(other, False)
  1026. def __add__(self, other: datetime.timedelta) -> Self:
  1027. if not isinstance(other, datetime.timedelta):
  1028. return NotImplemented
  1029. caller = traceback.extract_stack(limit=2)[0].name
  1030. if caller == "astimezone":
  1031. return super().__add__(other)
  1032. return self._add_timedelta_(other)
  1033. def __radd__(self, other: datetime.timedelta) -> Self:
  1034. return self.__add__(other)
  1035. # Native methods override
  1036. @classmethod
  1037. def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> Self:
  1038. tzinfo = pendulum._safe_timezone(tz)
  1039. return cls.instance(datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo)
  1040. @classmethod
  1041. def utcfromtimestamp(cls, t: float) -> Self:
  1042. return cls.instance(datetime.datetime.utcfromtimestamp(t), tz=None)
  1043. @classmethod
  1044. def fromordinal(cls, n: int) -> Self:
  1045. return cls.instance(datetime.datetime.fromordinal(n), tz=None)
  1046. @classmethod
  1047. def combine(
  1048. cls,
  1049. date: datetime.date,
  1050. time: datetime.time,
  1051. tzinfo: datetime.tzinfo | None = None,
  1052. ) -> Self:
  1053. return cls.instance(datetime.datetime.combine(date, time), tz=tzinfo)
  1054. def astimezone(self, tz: datetime.tzinfo | None = None) -> Self:
  1055. dt = super().astimezone(tz)
  1056. return self.__class__(
  1057. dt.year,
  1058. dt.month,
  1059. dt.day,
  1060. dt.hour,
  1061. dt.minute,
  1062. dt.second,
  1063. dt.microsecond,
  1064. fold=dt.fold,
  1065. tzinfo=dt.tzinfo,
  1066. )
  1067. def replace(
  1068. self,
  1069. year: SupportsIndex | None = None,
  1070. month: SupportsIndex | None = None,
  1071. day: SupportsIndex | None = None,
  1072. hour: SupportsIndex | None = None,
  1073. minute: SupportsIndex | None = None,
  1074. second: SupportsIndex | None = None,
  1075. microsecond: SupportsIndex | None = None,
  1076. tzinfo: bool | datetime.tzinfo | Literal[True] | None = True,
  1077. fold: int | None = None,
  1078. ) -> Self:
  1079. if year is None:
  1080. year = self.year
  1081. if month is None:
  1082. month = self.month
  1083. if day is None:
  1084. day = self.day
  1085. if hour is None:
  1086. hour = self.hour
  1087. if minute is None:
  1088. minute = self.minute
  1089. if second is None:
  1090. second = self.second
  1091. if microsecond is None:
  1092. microsecond = self.microsecond
  1093. if tzinfo is True:
  1094. tzinfo = self.tzinfo
  1095. if fold is None:
  1096. fold = self.fold
  1097. if tzinfo is not None:
  1098. tzinfo = pendulum._safe_timezone(tzinfo)
  1099. return self.__class__.create(
  1100. year,
  1101. month,
  1102. day,
  1103. hour,
  1104. minute,
  1105. second,
  1106. microsecond,
  1107. tz=tzinfo,
  1108. fold=fold,
  1109. )
  1110. def __getnewargs__(self) -> tuple[Self]:
  1111. return (self,)
  1112. def _getstate(
  1113. self, protocol: SupportsIndex = 3
  1114. ) -> tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]:
  1115. return (
  1116. self.year,
  1117. self.month,
  1118. self.day,
  1119. self.hour,
  1120. self.minute,
  1121. self.second,
  1122. self.microsecond,
  1123. self.tzinfo,
  1124. )
  1125. def __reduce__(
  1126. self,
  1127. ) -> tuple[
  1128. type[Self],
  1129. tuple[int, int, int, int, int, int, int, datetime.tzinfo | None],
  1130. ]:
  1131. return self.__reduce_ex__(2)
  1132. def __reduce_ex__(
  1133. self, protocol: SupportsIndex
  1134. ) -> tuple[
  1135. type[Self],
  1136. tuple[int, int, int, int, int, int, int, datetime.tzinfo | None],
  1137. ]:
  1138. return self.__class__, self._getstate(protocol)
  1139. def __deepcopy__(self, _: dict[int, Self]) -> Self:
  1140. return self.__class__(
  1141. self.year,
  1142. self.month,
  1143. self.day,
  1144. self.hour,
  1145. self.minute,
  1146. self.second,
  1147. self.microsecond,
  1148. tzinfo=self.tz,
  1149. fold=self.fold,
  1150. )
  1151. def _cmp(self, other: datetime.datetime, **kwargs: Any) -> int:
  1152. # Fix for pypy which compares using this method
  1153. # which would lead to infinite recursion if we didn't override
  1154. dt = datetime.datetime(
  1155. self.year,
  1156. self.month,
  1157. self.day,
  1158. self.hour,
  1159. self.minute,
  1160. self.second,
  1161. self.microsecond,
  1162. tzinfo=self.tz,
  1163. fold=self.fold,
  1164. )
  1165. return 0 if dt == other else 1 if dt > other else -1
  1166. DateTime.min = DateTime(1, 1, 1, 0, 0, tzinfo=UTC)
  1167. DateTime.max = DateTime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC)
  1168. DateTime.EPOCH = DateTime(1970, 1, 1, tzinfo=UTC)