Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pendulum/date.py: 36%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# The following is only needed because of Python 3.7
2# mypy: no-warn-unused-ignores
3from __future__ import annotations
5import calendar
6import math
8from datetime import date
9from datetime import datetime
10from datetime import timedelta
11from typing import TYPE_CHECKING
12from typing import ClassVar
13from typing import NoReturn
14from typing import cast
15from typing import overload
17import pendulum
19from pendulum.constants import MONTHS_PER_YEAR
20from pendulum.constants import YEARS_PER_CENTURY
21from pendulum.constants import YEARS_PER_DECADE
22from pendulum.day import WeekDay
23from pendulum.exceptions import PendulumException
24from pendulum.helpers import add_duration
25from pendulum.interval import Interval
26from pendulum.mixins.default import FormattableMixin
29if TYPE_CHECKING:
30 from typing_extensions import Self
31 from typing_extensions import SupportsIndex
34class Date(FormattableMixin, date):
35 _MODIFIERS_VALID_UNITS: ClassVar[list[str]] = [
36 "day",
37 "week",
38 "month",
39 "year",
40 "decade",
41 "century",
42 ]
44 # Getters/Setters
46 def set(
47 self, year: int | None = None, month: int | None = None, day: int | None = None
48 ) -> Self:
49 return self.replace(year=year, month=month, day=day)
51 @property
52 def day_of_week(self) -> WeekDay:
53 """
54 Returns the day of the week (0-6).
55 """
56 return WeekDay(self.weekday())
58 @property
59 def day_of_year(self) -> int:
60 """
61 Returns the day of the year (1-366).
62 """
63 k = 1 if self.is_leap_year() else 2
65 return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30
67 @property
68 def week_of_year(self) -> int:
69 return self.isocalendar()[1]
71 @property
72 def days_in_month(self) -> int:
73 return calendar.monthrange(self.year, self.month)[1]
75 @property
76 def week_of_month(self) -> int:
77 return math.ceil((self.day + self.first_of("month").isoweekday() - 1) / 7)
79 @property
80 def age(self) -> int:
81 return self.diff(abs=False).in_years()
83 @property
84 def quarter(self) -> int:
85 return math.ceil(self.month / 3)
87 # String Formatting
89 def to_date_string(self) -> str:
90 """
91 Format the instance as date.
93 :rtype: str
94 """
95 return self.strftime("%Y-%m-%d")
97 def to_formatted_date_string(self) -> str:
98 """
99 Format the instance as a readable date.
101 :rtype: str
102 """
103 return self.strftime("%b %d, %Y")
105 def __repr__(self) -> str:
106 return f"{self.__class__.__name__}({self.year}, {self.month}, {self.day})"
108 # COMPARISONS
110 def closest(self, dt1: date, dt2: date) -> Self:
111 """
112 Get the closest date from the instance.
113 """
114 dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
115 dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
117 if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
118 return dt1
120 return dt2
122 def farthest(self, dt1: date, dt2: date) -> Self:
123 """
124 Get the farthest date from the instance.
125 """
126 dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
127 dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
129 if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
130 return dt1
132 return dt2
134 def is_future(self) -> bool:
135 """
136 Determines if the instance is in the future, ie. greater than now.
137 """
138 return self > self.today()
140 def is_past(self) -> bool:
141 """
142 Determines if the instance is in the past, ie. less than now.
143 """
144 return self < self.today()
146 def is_leap_year(self) -> bool:
147 """
148 Determines if the instance is a leap year.
149 """
150 return calendar.isleap(self.year)
152 def is_long_year(self) -> bool:
153 """
154 Determines if the instance is a long year
156 See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_
157 """
158 return Date(self.year, 12, 28).isocalendar()[1] == 53
160 def is_same_day(self, dt: date) -> bool:
161 """
162 Checks if the passed in date is the same day as the instance current day.
163 """
164 return self == dt
166 def is_anniversary(self, dt: date | None = None) -> bool:
167 """
168 Check if it's the anniversary.
170 Compares the date/month values of the two dates.
171 """
172 if dt is None:
173 dt = self.__class__.today()
175 instance = self.__class__(dt.year, dt.month, dt.day)
177 return (self.month, self.day) == (instance.month, instance.day)
179 # the additional method for checking if today is the anniversary day
180 # the alias is provided to start using a new name and keep the backward
181 # compatibility the old name can be completely replaced with the new in
182 # one of the future versions
183 is_birthday = is_anniversary
185 # ADDITIONS AND SUBTRACTIONS
187 def add(
188 self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
189 ) -> Self:
190 """
191 Add duration to the instance.
193 :param years: The number of years
194 :param months: The number of months
195 :param weeks: The number of weeks
196 :param days: The number of days
197 """
198 dt = add_duration(
199 date(self.year, self.month, self.day),
200 years=years,
201 months=months,
202 weeks=weeks,
203 days=days,
204 )
206 return self.__class__(dt.year, dt.month, dt.day)
208 def subtract(
209 self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
210 ) -> Self:
211 """
212 Remove duration from the instance.
214 :param years: The number of years
215 :param months: The number of months
216 :param weeks: The number of weeks
217 :param days: The number of days
218 """
219 return self.add(years=-years, months=-months, weeks=-weeks, days=-days)
221 def _add_timedelta(self, delta: timedelta) -> Self:
222 """
223 Add timedelta duration to the instance.
225 :param delta: The timedelta instance
226 """
227 if isinstance(delta, pendulum.Duration):
228 return self.add(
229 years=delta.years,
230 months=delta.months,
231 weeks=delta.weeks,
232 days=delta.remaining_days,
233 )
235 return self.add(days=delta.days)
237 def _subtract_timedelta(self, delta: timedelta) -> Self:
238 """
239 Remove timedelta duration from the instance.
241 :param delta: The timedelta instance
242 """
243 if isinstance(delta, pendulum.Duration):
244 return self.subtract(
245 years=delta.years,
246 months=delta.months,
247 weeks=delta.weeks,
248 days=delta.remaining_days,
249 )
251 return self.subtract(days=delta.days)
253 def __add__(self, other: timedelta) -> Self:
254 if not isinstance(other, timedelta):
255 return NotImplemented
257 return self._add_timedelta(other)
259 @overload # type: ignore[override] # this is only needed because of Python 3.7
260 def __sub__(self, __delta: timedelta) -> Self: ...
262 @overload
263 def __sub__(self, __dt: datetime) -> NoReturn: ...
265 @overload
266 def __sub__(self, __dt: Self) -> Interval[Date]: ...
268 def __sub__(self, other: timedelta | date) -> Self | Interval[Date]:
269 if isinstance(other, timedelta):
270 return self._subtract_timedelta(other)
272 if not isinstance(other, date):
273 return NotImplemented
275 dt = self.__class__(other.year, other.month, other.day)
277 return dt.diff(self, False)
279 # DIFFERENCES
281 def diff(self, dt: date | None = None, abs: bool = True) -> Interval[Date]:
282 """
283 Returns the difference between two Date objects as an Interval.
285 :param dt: The date to compare to (defaults to today)
286 :param abs: Whether to return an absolute interval or not
287 """
288 if dt is None:
289 dt = self.today()
291 return Interval(self, Date(dt.year, dt.month, dt.day), absolute=abs)
293 def diff_for_humans(
294 self,
295 other: date | None = None,
296 absolute: bool = False,
297 locale: str | None = None,
298 ) -> str:
299 """
300 Get the difference in a human readable format in the current locale.
302 When comparing a value in the past to default now:
303 1 day ago
304 5 months ago
306 When comparing a value in the future to default now:
307 1 day from now
308 5 months from now
310 When comparing a value in the past to another value:
311 1 day before
312 5 months before
314 When comparing a value in the future to another value:
315 1 day after
316 5 months after
318 :param other: The date to compare to (defaults to today)
319 :param absolute: removes time difference modifiers ago, after, etc
320 :param locale: The locale to use for localization
321 """
322 is_now = other is None
324 if is_now:
325 other = self.today()
327 diff = self.diff(other)
329 return pendulum.format_diff(diff, is_now, absolute, locale)
331 # MODIFIERS
333 def start_of(self, unit: str) -> Self:
334 """
335 Returns a copy of the instance with the time reset
336 with the following rules:
338 * day: time to 00:00:00
339 * week: date to first day of the week and time to 00:00:00
340 * month: date to first day of the month and time to 00:00:00
341 * year: date to first day of the year and time to 00:00:00
342 * decade: date to first day of the decade and time to 00:00:00
343 * century: date to first day of century and time to 00:00:00
345 :param unit: The unit to reset to
346 """
347 if unit not in self._MODIFIERS_VALID_UNITS:
348 raise ValueError(f'Invalid unit "{unit}" for start_of()')
350 return cast("Self", getattr(self, f"_start_of_{unit}")())
352 def end_of(self, unit: str) -> Self:
353 """
354 Returns a copy of the instance with the time reset
355 with the following rules:
357 * week: date to last day of the week
358 * month: date to last day of the month
359 * year: date to last day of the year
360 * decade: date to last day of the decade
361 * century: date to last day of century
363 :param unit: The unit to reset to
364 """
365 if unit not in self._MODIFIERS_VALID_UNITS:
366 raise ValueError(f'Invalid unit "{unit}" for end_of()')
368 return cast("Self", getattr(self, f"_end_of_{unit}")())
370 def _start_of_day(self) -> Self:
371 """
372 Compatibility method.
373 """
374 return self
376 def _end_of_day(self) -> Self:
377 """
378 Compatibility method
379 """
380 return self
382 def _start_of_month(self) -> Self:
383 """
384 Reset the date to the first day of the month.
385 """
386 return self.set(self.year, self.month, 1)
388 def _end_of_month(self) -> Self:
389 """
390 Reset the date to the last day of the month.
391 """
392 return self.set(self.year, self.month, self.days_in_month)
394 def _start_of_year(self) -> Self:
395 """
396 Reset the date to the first day of the year.
397 """
398 return self.set(self.year, 1, 1)
400 def _end_of_year(self) -> Self:
401 """
402 Reset the date to the last day of the year.
403 """
404 return self.set(self.year, 12, 31)
406 def _start_of_decade(self) -> Self:
407 """
408 Reset the date to the first day of the decade.
409 """
410 year = self.year - self.year % YEARS_PER_DECADE
412 return self.set(year, 1, 1)
414 def _end_of_decade(self) -> Self:
415 """
416 Reset the date to the last day of the decade.
417 """
418 year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
420 return self.set(year, 12, 31)
422 def _start_of_century(self) -> Self:
423 """
424 Reset the date to the first day of the century.
425 """
426 year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
428 return self.set(year, 1, 1)
430 def _end_of_century(self) -> Self:
431 """
432 Reset the date to the last day of the century.
433 """
434 year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
436 return self.set(year, 12, 31)
438 def _start_of_week(self) -> Self:
439 """
440 Reset the date to the first day of the week.
441 """
442 dt = self
444 if self.day_of_week != pendulum._WEEK_STARTS_AT:
445 dt = self.previous(pendulum._WEEK_STARTS_AT)
447 return dt.start_of("day")
449 def _end_of_week(self) -> Self:
450 """
451 Reset the date to the last day of the week.
452 """
453 dt = self
455 if self.day_of_week != pendulum._WEEK_ENDS_AT:
456 dt = self.next(pendulum._WEEK_ENDS_AT)
458 return dt.end_of("day")
460 def next(self, day_of_week: WeekDay | None = None) -> Self:
461 """
462 Modify to the next occurrence of a given day of the week.
463 If no day_of_week is provided, modify to the next occurrence
464 of the current day of the week. Use the supplied consts
465 to indicate the desired day_of_week, ex. pendulum.MONDAY.
467 :param day_of_week: The next day of week to reset to.
468 """
469 if day_of_week is None:
470 day_of_week = self.day_of_week
472 if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY:
473 raise ValueError("Invalid day of week")
475 dt = self.add(days=1)
476 while dt.day_of_week != day_of_week:
477 dt = dt.add(days=1)
479 return dt
481 def previous(self, day_of_week: WeekDay | None = None) -> Self:
482 """
483 Modify to the previous occurrence of a given day of the week.
484 If no day_of_week is provided, modify to the previous occurrence
485 of the current day of the week. Use the supplied consts
486 to indicate the desired day_of_week, ex. pendulum.MONDAY.
488 :param day_of_week: The previous day of week to reset to.
489 """
490 if day_of_week is None:
491 day_of_week = self.day_of_week
493 if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY:
494 raise ValueError("Invalid day of week")
496 dt = self.subtract(days=1)
497 while dt.day_of_week != day_of_week:
498 dt = dt.subtract(days=1)
500 return dt
502 def first_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self:
503 """
504 Returns an instance set to the first occurrence
505 of a given day of the week in the current unit.
506 If no day_of_week is provided, modify to the first day of the unit.
507 Use the supplied consts to indicate the desired day_of_week,
508 ex. pendulum.MONDAY.
510 Supported units are month, quarter and year.
512 :param unit: The unit to use
513 :param day_of_week: The day of week to reset to.
514 """
515 if unit not in ["month", "quarter", "year"]:
516 raise ValueError(f'Invalid unit "{unit}" for first_of()')
518 return cast("Self", getattr(self, f"_first_of_{unit}")(day_of_week))
520 def last_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self:
521 """
522 Returns an instance set to the last occurrence
523 of a given day of the week in the current unit.
524 If no day_of_week is provided, modify to the last day of the unit.
525 Use the supplied consts to indicate the desired day_of_week,
526 ex. pendulum.MONDAY.
528 Supported units are month, quarter and year.
530 :param unit: The unit to use
531 :param day_of_week: The day of week to reset to.
532 """
533 if unit not in ["month", "quarter", "year"]:
534 raise ValueError(f'Invalid unit "{unit}" for first_of()')
536 return cast("Self", getattr(self, f"_last_of_{unit}")(day_of_week))
538 def nth_of(self, unit: str, nth: int, day_of_week: WeekDay) -> Self:
539 """
540 Returns a new instance set to the given occurrence
541 of a given day of the week in the current unit.
542 If the calculated occurrence is outside the scope of the current unit,
543 then raise an error. Use the supplied consts
544 to indicate the desired day_of_week, ex. pendulum.MONDAY.
546 Supported units are month, quarter and year.
548 :param unit: The unit to use
549 :param nth: The occurrence to use
550 :param day_of_week: The day of week to set to.
551 """
552 if unit not in ["month", "quarter", "year"]:
553 raise ValueError(f'Invalid unit "{unit}" for first_of()')
555 dt = cast("Self", getattr(self, f"_nth_of_{unit}")(nth, day_of_week))
556 if not dt:
557 raise PendulumException(
558 f"Unable to find occurrence {nth}"
559 f" of {WeekDay(day_of_week).name.capitalize()} in {unit}"
560 )
562 return dt
564 def _first_of_month(self, day_of_week: WeekDay) -> Self:
565 """
566 Modify to the first occurrence of a given day of the week
567 in the current month. If no day_of_week is provided,
568 modify to the first day of the month. Use the supplied consts
569 to indicate the desired day_of_week, ex. pendulum.MONDAY.
571 :param day_of_week: The day of week to set to.
572 """
573 dt = self
575 if day_of_week is None:
576 return dt.set(day=1)
578 month = calendar.monthcalendar(dt.year, dt.month)
580 calendar_day = day_of_week
582 if month[0][calendar_day] > 0:
583 day_of_month = month[0][calendar_day]
584 else:
585 day_of_month = month[1][calendar_day]
587 return dt.set(day=day_of_month)
589 def _last_of_month(self, day_of_week: WeekDay | None = None) -> Self:
590 """
591 Modify to the last occurrence of a given day of the week
592 in the current month. If no day_of_week is provided,
593 modify to the last day of the month. Use the supplied consts
594 to indicate the desired day_of_week, ex. pendulum.MONDAY.
596 :param day_of_week: The day of week to set to.
597 """
598 dt = self
600 if day_of_week is None:
601 return dt.set(day=self.days_in_month)
603 month = calendar.monthcalendar(dt.year, dt.month)
605 calendar_day = day_of_week
607 if month[-1][calendar_day] > 0:
608 day_of_month = month[-1][calendar_day]
609 else:
610 day_of_month = month[-2][calendar_day]
612 return dt.set(day=day_of_month)
614 def _nth_of_month(self, nth: int, day_of_week: WeekDay) -> Self | None:
615 """
616 Modify to the given occurrence of a given day of the week
617 in the current month. If the calculated occurrence is outside,
618 the scope of the current month, then return False and no
619 modifications are made. Use the supplied consts
620 to indicate the desired day_of_week, ex. pendulum.MONDAY.
621 """
622 if nth == 1:
623 return self.first_of("month", day_of_week)
625 dt = self.first_of("month")
626 check = dt.format("YYYY-MM")
627 for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
628 dt = dt.next(day_of_week)
630 if dt.format("YYYY-MM") == check:
631 return self.set(day=dt.day)
633 return None
635 def _first_of_quarter(self, day_of_week: WeekDay | None = None) -> Self:
636 """
637 Modify to the first occurrence of a given day of the week
638 in the current quarter. If no day_of_week is provided,
639 modify to the first day of the quarter. Use the supplied consts
640 to indicate the desired day_of_week, ex. pendulum.MONDAY.
641 """
642 return self.set(self.year, self.quarter * 3 - 2, 1).first_of(
643 "month", day_of_week
644 )
646 def _last_of_quarter(self, day_of_week: WeekDay | None = None) -> Self:
647 """
648 Modify to the last occurrence of a given day of the week
649 in the current quarter. If no day_of_week is provided,
650 modify to the last day of the quarter. Use the supplied consts
651 to indicate the desired day_of_week, ex. pendulum.MONDAY.
652 """
653 return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
655 def _nth_of_quarter(self, nth: int, day_of_week: WeekDay) -> Self | None:
656 """
657 Modify to the given occurrence of a given day of the week
658 in the current quarter. If the calculated occurrence is outside,
659 the scope of the current quarter, then return False and no
660 modifications are made. Use the supplied consts
661 to indicate the desired day_of_week, ex. pendulum.MONDAY.
662 """
663 if nth == 1:
664 return self.first_of("quarter", day_of_week)
666 dt = self.replace(self.year, self.quarter * 3, 1)
667 last_month = dt.month
668 year = dt.year
669 dt = dt.first_of("quarter")
670 for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
671 dt = dt.next(day_of_week)
673 if last_month < dt.month or year != dt.year:
674 return None
676 return self.set(self.year, dt.month, dt.day)
678 def _first_of_year(self, day_of_week: WeekDay | None = None) -> Self:
679 """
680 Modify to the first occurrence of a given day of the week
681 in the current year. If no day_of_week is provided,
682 modify to the first day of the year. Use the supplied consts
683 to indicate the desired day_of_week, ex. pendulum.MONDAY.
684 """
685 return self.set(month=1).first_of("month", day_of_week)
687 def _last_of_year(self, day_of_week: WeekDay | None = None) -> Self:
688 """
689 Modify to the last occurrence of a given day of the week
690 in the current year. If no day_of_week is provided,
691 modify to the last day of the year. Use the supplied consts
692 to indicate the desired day_of_week, ex. pendulum.MONDAY.
693 """
694 return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
696 def _nth_of_year(self, nth: int, day_of_week: WeekDay) -> Self | None:
697 """
698 Modify to the given occurrence of a given day of the week
699 in the current year. If the calculated occurrence is outside,
700 the scope of the current year, then return False and no
701 modifications are made. Use the supplied consts
702 to indicate the desired day_of_week, ex. pendulum.MONDAY.
703 """
704 if nth == 1:
705 return self.first_of("year", day_of_week)
707 dt = self.first_of("year")
708 year = dt.year
709 for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
710 dt = dt.next(day_of_week)
712 if year != dt.year:
713 return None
715 return self.set(self.year, dt.month, dt.day)
717 def average(self, dt: date | None = None) -> Self:
718 """
719 Modify the current instance to the average
720 of a given instance (default now) and the current instance.
721 """
722 if dt is None:
723 dt = Date.today()
725 return self.add(days=int(self.diff(dt, False).in_days() / 2))
727 # Native methods override
729 @classmethod
730 def today(cls) -> Self:
731 dt = date.today()
733 return cls(dt.year, dt.month, dt.day)
735 @classmethod
736 def fromtimestamp(cls, t: float) -> Self:
737 dt = super().fromtimestamp(t)
739 return cls(dt.year, dt.month, dt.day)
741 @classmethod
742 def fromordinal(cls, n: int) -> Self:
743 dt = super().fromordinal(n)
745 return cls(dt.year, dt.month, dt.day)
747 def replace(
748 self,
749 year: SupportsIndex | None = None,
750 month: SupportsIndex | None = None,
751 day: SupportsIndex | None = None,
752 ) -> Self:
753 year = year if year is not None else self.year
754 month = month if month is not None else self.month
755 day = day if day is not None else self.day
757 return self.__class__(year, month, day)