Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pendulum/date.py: 35%
283 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 06:11 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 06:11 +0000
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 first_day_of_month = self.replace(day=1)
79 return self.week_of_year - first_day_of_month.week_of_year + 1
81 @property
82 def age(self) -> int:
83 return self.diff(abs=False).in_years()
85 @property
86 def quarter(self) -> int:
87 return math.ceil(self.month / 3)
89 # String Formatting
91 def to_date_string(self) -> str:
92 """
93 Format the instance as date.
95 :rtype: str
96 """
97 return self.strftime("%Y-%m-%d")
99 def to_formatted_date_string(self) -> str:
100 """
101 Format the instance as a readable date.
103 :rtype: str
104 """
105 return self.strftime("%b %d, %Y")
107 def __repr__(self) -> str:
108 return f"{self.__class__.__name__}({self.year}, {self.month}, {self.day})"
110 # COMPARISONS
112 def closest(self, dt1: date, dt2: date) -> Self:
113 """
114 Get the closest date from the instance.
115 """
116 dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
117 dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
119 if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
120 return dt1
122 return dt2
124 def farthest(self, dt1: date, dt2: date) -> Self:
125 """
126 Get the farthest date from the instance.
127 """
128 dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
129 dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
131 if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
132 return dt1
134 return dt2
136 def is_future(self) -> bool:
137 """
138 Determines if the instance is in the future, ie. greater than now.
139 """
140 return self > self.today()
142 def is_past(self) -> bool:
143 """
144 Determines if the instance is in the past, ie. less than now.
145 """
146 return self < self.today()
148 def is_leap_year(self) -> bool:
149 """
150 Determines if the instance is a leap year.
151 """
152 return calendar.isleap(self.year)
154 def is_long_year(self) -> bool:
155 """
156 Determines if the instance is a long year
158 See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_
159 """
160 return Date(self.year, 12, 28).isocalendar()[1] == 53
162 def is_same_day(self, dt: date) -> bool:
163 """
164 Checks if the passed in date is the same day as the instance current day.
165 """
166 return self == dt
168 def is_anniversary(self, dt: date | None = None) -> bool:
169 """
170 Check if it's the anniversary.
172 Compares the date/month values of the two dates.
173 """
174 if dt is None:
175 dt = self.__class__.today()
177 instance = self.__class__(dt.year, dt.month, dt.day)
179 return (self.month, self.day) == (instance.month, instance.day)
181 # the additional method for checking if today is the anniversary day
182 # the alias is provided to start using a new name and keep the backward
183 # compatibility the old name can be completely replaced with the new in
184 # one of the future versions
185 is_birthday = is_anniversary
187 # ADDITIONS AND SUBTRACTIONS
189 def add(
190 self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
191 ) -> Self:
192 """
193 Add duration to the instance.
195 :param years: The number of years
196 :param months: The number of months
197 :param weeks: The number of weeks
198 :param days: The number of days
199 """
200 dt = add_duration(
201 date(self.year, self.month, self.day),
202 years=years,
203 months=months,
204 weeks=weeks,
205 days=days,
206 )
208 return self.__class__(dt.year, dt.month, dt.day)
210 def subtract(
211 self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
212 ) -> Self:
213 """
214 Remove duration from the instance.
216 :param years: The number of years
217 :param months: The number of months
218 :param weeks: The number of weeks
219 :param days: The number of days
220 """
221 return self.add(years=-years, months=-months, weeks=-weeks, days=-days)
223 def _add_timedelta(self, delta: timedelta) -> Self:
224 """
225 Add timedelta duration to the instance.
227 :param delta: The timedelta instance
228 """
229 if isinstance(delta, pendulum.Duration):
230 return self.add(
231 years=delta.years,
232 months=delta.months,
233 weeks=delta.weeks,
234 days=delta.remaining_days,
235 )
237 return self.add(days=delta.days)
239 def _subtract_timedelta(self, delta: timedelta) -> Self:
240 """
241 Remove timedelta duration from the instance.
243 :param delta: The timedelta instance
244 """
245 if isinstance(delta, pendulum.Duration):
246 return self.subtract(
247 years=delta.years,
248 months=delta.months,
249 weeks=delta.weeks,
250 days=delta.remaining_days,
251 )
253 return self.subtract(days=delta.days)
255 def __add__(self, other: timedelta) -> Self:
256 if not isinstance(other, timedelta):
257 return NotImplemented
259 return self._add_timedelta(other)
261 @overload # type: ignore[override] # this is only needed because of Python 3.7
262 def __sub__(self, __delta: timedelta) -> Self:
263 ...
265 @overload
266 def __sub__(self, __dt: datetime) -> NoReturn:
267 ...
269 @overload
270 def __sub__(self, __dt: Self) -> Interval:
271 ...
273 def __sub__(self, other: timedelta | date) -> Self | Interval:
274 if isinstance(other, timedelta):
275 return self._subtract_timedelta(other)
277 if not isinstance(other, date):
278 return NotImplemented
280 dt = self.__class__(other.year, other.month, other.day)
282 return dt.diff(self, False)
284 # DIFFERENCES
286 def diff(self, dt: date | None = None, abs: bool = True) -> Interval:
287 """
288 Returns the difference between two Date objects as an Interval.
290 :param dt: The date to compare to (defaults to today)
291 :param abs: Whether to return an absolute interval or not
292 """
293 if dt is None:
294 dt = self.today()
296 return Interval(self, Date(dt.year, dt.month, dt.day), absolute=abs)
298 def diff_for_humans(
299 self,
300 other: date | None = None,
301 absolute: bool = False,
302 locale: str | None = None,
303 ) -> str:
304 """
305 Get the difference in a human readable format in the current locale.
307 When comparing a value in the past to default now:
308 1 day ago
309 5 months ago
311 When comparing a value in the future to default now:
312 1 day from now
313 5 months from now
315 When comparing a value in the past to another value:
316 1 day before
317 5 months before
319 When comparing a value in the future to another value:
320 1 day after
321 5 months after
323 :param other: The date to compare to (defaults to today)
324 :param absolute: removes time difference modifiers ago, after, etc
325 :param locale: The locale to use for localization
326 """
327 is_now = other is None
329 if is_now:
330 other = self.today()
332 diff = self.diff(other)
334 return pendulum.format_diff(diff, is_now, absolute, locale)
336 # MODIFIERS
338 def start_of(self, unit: str) -> Self:
339 """
340 Returns a copy of the instance with the time reset
341 with the following rules:
343 * day: time to 00:00:00
344 * week: date to first day of the week and time to 00:00:00
345 * month: date to first day of the month and time to 00:00:00
346 * year: date to first day of the year and time to 00:00:00
347 * decade: date to first day of the decade and time to 00:00:00
348 * century: date to first day of century and time to 00:00:00
350 :param unit: The unit to reset to
351 """
352 if unit not in self._MODIFIERS_VALID_UNITS:
353 raise ValueError(f'Invalid unit "{unit}" for start_of()')
355 return cast("Self", getattr(self, f"_start_of_{unit}")())
357 def end_of(self, unit: str) -> Self:
358 """
359 Returns a copy of the instance with the time reset
360 with the following rules:
362 * week: date to last day of the week
363 * month: date to last day of the month
364 * year: date to last day of the year
365 * decade: date to last day of the decade
366 * century: date to last day of century
368 :param unit: The unit to reset to
369 """
370 if unit not in self._MODIFIERS_VALID_UNITS:
371 raise ValueError(f'Invalid unit "{unit}" for end_of()')
373 return cast("Self", getattr(self, f"_end_of_{unit}")())
375 def _start_of_day(self) -> Self:
376 """
377 Compatibility method.
378 """
379 return self
381 def _end_of_day(self) -> Self:
382 """
383 Compatibility method
384 """
385 return self
387 def _start_of_month(self) -> Self:
388 """
389 Reset the date to the first day of the month.
390 """
391 return self.set(self.year, self.month, 1)
393 def _end_of_month(self) -> Self:
394 """
395 Reset the date to the last day of the month.
396 """
397 return self.set(self.year, self.month, self.days_in_month)
399 def _start_of_year(self) -> Self:
400 """
401 Reset the date to the first day of the year.
402 """
403 return self.set(self.year, 1, 1)
405 def _end_of_year(self) -> Self:
406 """
407 Reset the date to the last day of the year.
408 """
409 return self.set(self.year, 12, 31)
411 def _start_of_decade(self) -> Self:
412 """
413 Reset the date to the first day of the decade.
414 """
415 year = self.year - self.year % YEARS_PER_DECADE
417 return self.set(year, 1, 1)
419 def _end_of_decade(self) -> Self:
420 """
421 Reset the date to the last day of the decade.
422 """
423 year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
425 return self.set(year, 12, 31)
427 def _start_of_century(self) -> Self:
428 """
429 Reset the date to the first day of the century.
430 """
431 year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
433 return self.set(year, 1, 1)
435 def _end_of_century(self) -> Self:
436 """
437 Reset the date to the last day of the century.
438 """
439 year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
441 return self.set(year, 12, 31)
443 def _start_of_week(self) -> Self:
444 """
445 Reset the date to the first day of the week.
446 """
447 dt = self
449 if self.day_of_week != pendulum._WEEK_STARTS_AT:
450 dt = self.previous(pendulum._WEEK_STARTS_AT)
452 return dt.start_of("day")
454 def _end_of_week(self) -> Self:
455 """
456 Reset the date to the last day of the week.
457 """
458 dt = self
460 if self.day_of_week != pendulum._WEEK_ENDS_AT:
461 dt = self.next(pendulum._WEEK_ENDS_AT)
463 return dt.end_of("day")
465 def next(self, day_of_week: WeekDay | None = None) -> Self:
466 """
467 Modify to the next occurrence of a given day of the week.
468 If no day_of_week is provided, modify to the next occurrence
469 of the current day of the week. Use the supplied consts
470 to indicate the desired day_of_week, ex. pendulum.MONDAY.
472 :param day_of_week: The next day of week to reset to.
473 """
474 if day_of_week is None:
475 day_of_week = self.day_of_week
477 if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY:
478 raise ValueError("Invalid day of week")
480 dt = self.add(days=1)
481 while dt.day_of_week != day_of_week:
482 dt = dt.add(days=1)
484 return dt
486 def previous(self, day_of_week: WeekDay | None = None) -> Self:
487 """
488 Modify to the previous occurrence of a given day of the week.
489 If no day_of_week is provided, modify to the previous occurrence
490 of the current day of the week. Use the supplied consts
491 to indicate the desired day_of_week, ex. pendulum.MONDAY.
493 :param day_of_week: The previous day of week to reset to.
494 """
495 if day_of_week is None:
496 day_of_week = self.day_of_week
498 if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY:
499 raise ValueError("Invalid day of week")
501 dt = self.subtract(days=1)
502 while dt.day_of_week != day_of_week:
503 dt = dt.subtract(days=1)
505 return dt
507 def first_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self:
508 """
509 Returns an instance set to the first occurrence
510 of a given day of the week in the current unit.
511 If no day_of_week is provided, modify to the first day of the unit.
512 Use the supplied consts to indicate the desired day_of_week,
513 ex. pendulum.MONDAY.
515 Supported units are month, quarter and year.
517 :param unit: The unit to use
518 :param day_of_week: The day of week to reset to.
519 """
520 if unit not in ["month", "quarter", "year"]:
521 raise ValueError(f'Invalid unit "{unit}" for first_of()')
523 return cast("Self", getattr(self, f"_first_of_{unit}")(day_of_week))
525 def last_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self:
526 """
527 Returns an instance set to the last occurrence
528 of a given day of the week in the current unit.
529 If no day_of_week is provided, modify to the last day of the unit.
530 Use the supplied consts to indicate the desired day_of_week,
531 ex. pendulum.MONDAY.
533 Supported units are month, quarter and year.
535 :param unit: The unit to use
536 :param day_of_week: The day of week to reset to.
537 """
538 if unit not in ["month", "quarter", "year"]:
539 raise ValueError(f'Invalid unit "{unit}" for first_of()')
541 return cast("Self", getattr(self, f"_last_of_{unit}")(day_of_week))
543 def nth_of(self, unit: str, nth: int, day_of_week: WeekDay) -> Self:
544 """
545 Returns a new instance set to the given occurrence
546 of a given day of the week in the current unit.
547 If the calculated occurrence is outside the scope of the current unit,
548 then raise an error. Use the supplied consts
549 to indicate the desired day_of_week, ex. pendulum.MONDAY.
551 Supported units are month, quarter and year.
553 :param unit: The unit to use
554 :param nth: The occurrence to use
555 :param day_of_week: The day of week to set to.
556 """
557 if unit not in ["month", "quarter", "year"]:
558 raise ValueError(f'Invalid unit "{unit}" for first_of()')
560 dt = cast("Self", getattr(self, f"_nth_of_{unit}")(nth, day_of_week))
561 if not dt:
562 raise PendulumException(
563 f"Unable to find occurrence {nth}"
564 f" of {WeekDay(day_of_week).name.capitalize()} in {unit}"
565 )
567 return dt
569 def _first_of_month(self, day_of_week: WeekDay) -> Self:
570 """
571 Modify to the first occurrence of a given day of the week
572 in the current month. If no day_of_week is provided,
573 modify to the first day of the month. Use the supplied consts
574 to indicate the desired day_of_week, ex. pendulum.MONDAY.
576 :param day_of_week: The day of week to set to.
577 """
578 dt = self
580 if day_of_week is None:
581 return dt.set(day=1)
583 month = calendar.monthcalendar(dt.year, dt.month)
585 calendar_day = day_of_week
587 if month[0][calendar_day] > 0:
588 day_of_month = month[0][calendar_day]
589 else:
590 day_of_month = month[1][calendar_day]
592 return dt.set(day=day_of_month)
594 def _last_of_month(self, day_of_week: WeekDay | None = None) -> Self:
595 """
596 Modify to the last occurrence of a given day of the week
597 in the current month. If no day_of_week is provided,
598 modify to the last day of the month. Use the supplied consts
599 to indicate the desired day_of_week, ex. pendulum.MONDAY.
601 :param day_of_week: The day of week to set to.
602 """
603 dt = self
605 if day_of_week is None:
606 return dt.set(day=self.days_in_month)
608 month = calendar.monthcalendar(dt.year, dt.month)
610 calendar_day = day_of_week
612 if month[-1][calendar_day] > 0:
613 day_of_month = month[-1][calendar_day]
614 else:
615 day_of_month = month[-2][calendar_day]
617 return dt.set(day=day_of_month)
619 def _nth_of_month(self, nth: int, day_of_week: WeekDay) -> Self | None:
620 """
621 Modify to the given occurrence of a given day of the week
622 in the current month. If the calculated occurrence is outside,
623 the scope of the current month, then return False and no
624 modifications are made. Use the supplied consts
625 to indicate the desired day_of_week, ex. pendulum.MONDAY.
626 """
627 if nth == 1:
628 return self.first_of("month", day_of_week)
630 dt = self.first_of("month")
631 check = dt.format("YYYY-MM")
632 for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
633 dt = dt.next(day_of_week)
635 if dt.format("YYYY-MM") == check:
636 return self.set(day=dt.day)
638 return None
640 def _first_of_quarter(self, day_of_week: WeekDay | None = None) -> Self:
641 """
642 Modify to the first occurrence of a given day of the week
643 in the current quarter. If no day_of_week is provided,
644 modify to the first day of the quarter. Use the supplied consts
645 to indicate the desired day_of_week, ex. pendulum.MONDAY.
646 """
647 return self.set(self.year, self.quarter * 3 - 2, 1).first_of(
648 "month", day_of_week
649 )
651 def _last_of_quarter(self, day_of_week: WeekDay | None = None) -> Self:
652 """
653 Modify to the last occurrence of a given day of the week
654 in the current quarter. If no day_of_week is provided,
655 modify to the last day of the quarter. Use the supplied consts
656 to indicate the desired day_of_week, ex. pendulum.MONDAY.
657 """
658 return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
660 def _nth_of_quarter(self, nth: int, day_of_week: WeekDay) -> Self | None:
661 """
662 Modify to the given occurrence of a given day of the week
663 in the current quarter. If the calculated occurrence is outside,
664 the scope of the current quarter, then return False and no
665 modifications are made. Use the supplied consts
666 to indicate the desired day_of_week, ex. pendulum.MONDAY.
667 """
668 if nth == 1:
669 return self.first_of("quarter", day_of_week)
671 dt = self.replace(self.year, self.quarter * 3, 1)
672 last_month = dt.month
673 year = dt.year
674 dt = dt.first_of("quarter")
675 for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
676 dt = dt.next(day_of_week)
678 if last_month < dt.month or year != dt.year:
679 return None
681 return self.set(self.year, dt.month, dt.day)
683 def _first_of_year(self, day_of_week: WeekDay | None = None) -> Self:
684 """
685 Modify to the first occurrence of a given day of the week
686 in the current year. If no day_of_week is provided,
687 modify to the first day of the year. Use the supplied consts
688 to indicate the desired day_of_week, ex. pendulum.MONDAY.
689 """
690 return self.set(month=1).first_of("month", day_of_week)
692 def _last_of_year(self, day_of_week: WeekDay | None = None) -> Self:
693 """
694 Modify to the last occurrence of a given day of the week
695 in the current year. If no day_of_week is provided,
696 modify to the last day of the year. Use the supplied consts
697 to indicate the desired day_of_week, ex. pendulum.MONDAY.
698 """
699 return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
701 def _nth_of_year(self, nth: int, day_of_week: WeekDay) -> Self | None:
702 """
703 Modify to the given occurrence of a given day of the week
704 in the current year. If the calculated occurrence is outside,
705 the scope of the current year, then return False and no
706 modifications are made. Use the supplied consts
707 to indicate the desired day_of_week, ex. pendulum.MONDAY.
708 """
709 if nth == 1:
710 return self.first_of("year", day_of_week)
712 dt = self.first_of("year")
713 year = dt.year
714 for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
715 dt = dt.next(day_of_week)
717 if year != dt.year:
718 return None
720 return self.set(self.year, dt.month, dt.day)
722 def average(self, dt: date | None = None) -> Self:
723 """
724 Modify the current instance to the average
725 of a given instance (default now) and the current instance.
726 """
727 if dt is None:
728 dt = Date.today()
730 return self.add(days=int(self.diff(dt, False).in_days() / 2))
732 # Native methods override
734 @classmethod
735 def today(cls) -> Self:
736 dt = date.today()
738 return cls(dt.year, dt.month, dt.day)
740 @classmethod
741 def fromtimestamp(cls, t: float) -> Self:
742 dt = super().fromtimestamp(t)
744 return cls(dt.year, dt.month, dt.day)
746 @classmethod
747 def fromordinal(cls, n: int) -> Self:
748 dt = super().fromordinal(n)
750 return cls(dt.year, dt.month, dt.day)
752 def replace(
753 self,
754 year: SupportsIndex | None = None,
755 month: SupportsIndex | None = None,
756 day: SupportsIndex | None = None,
757 ) -> Self:
758 year = year if year is not None else self.year
759 month = month if month is not None else self.month
760 day = day if day is not None else self.day
762 return self.__class__(year, month, day)