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