summaryrefslogtreecommitdiffstats
path: root/pendulum/date.py
diff options
context:
space:
mode:
Diffstat (limited to 'pendulum/date.py')
-rw-r--r--pendulum/date.py1651
1 files changed, 760 insertions, 891 deletions
diff --git a/pendulum/date.py b/pendulum/date.py
index 41a9883..4e2655b 100644
--- a/pendulum/date.py
+++ b/pendulum/date.py
@@ -1,891 +1,760 @@
-from __future__ import absolute_import
-from __future__ import division
-
-import calendar
-import math
-
-from datetime import date
-from datetime import timedelta
-
-import pendulum
-
-from .constants import FRIDAY
-from .constants import MONDAY
-from .constants import MONTHS_PER_YEAR
-from .constants import SATURDAY
-from .constants import SUNDAY
-from .constants import THURSDAY
-from .constants import TUESDAY
-from .constants import WEDNESDAY
-from .constants import YEARS_PER_CENTURY
-from .constants import YEARS_PER_DECADE
-from .exceptions import PendulumException
-from .helpers import add_duration
-from .mixins.default import FormattableMixin
-from .period import Period
-
-
-class Date(FormattableMixin, date):
-
- # Names of days of the week
- _days = {
- SUNDAY: "Sunday",
- MONDAY: "Monday",
- TUESDAY: "Tuesday",
- WEDNESDAY: "Wednesday",
- THURSDAY: "Thursday",
- FRIDAY: "Friday",
- SATURDAY: "Saturday",
- }
-
- _MODIFIERS_VALID_UNITS = ["day", "week", "month", "year", "decade", "century"]
-
- # Getters/Setters
-
- def set(self, year=None, month=None, day=None):
- return self.replace(year=year, month=month, day=day)
-
- @property
- def day_of_week(self):
- """
- Returns the day of the week (0-6).
-
- :rtype: int
- """
- return self.isoweekday() % 7
-
- @property
- def day_of_year(self):
- """
- Returns the day of the year (1-366).
-
- :rtype: int
- """
- k = 1 if self.is_leap_year() else 2
-
- return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30
-
- @property
- def week_of_year(self):
- return self.isocalendar()[1]
-
- @property
- def days_in_month(self):
- return calendar.monthrange(self.year, self.month)[1]
-
- @property
- def week_of_month(self):
- first_day_of_month = self.replace(day=1)
-
- return self.week_of_year - first_day_of_month.week_of_year + 1
-
- @property
- def age(self):
- return self.diff(abs=False).in_years()
-
- @property
- def quarter(self):
- return int(math.ceil(self.month / 3))
-
- # String Formatting
-
- def to_date_string(self):
- """
- Format the instance as date.
-
- :rtype: str
- """
- return self.strftime("%Y-%m-%d")
-
- def to_formatted_date_string(self):
- """
- Format the instance as a readable date.
-
- :rtype: str
- """
- return self.strftime("%b %d, %Y")
-
- def __repr__(self):
- return (
- "{klass}("
- "{year}, {month}, {day}"
- ")".format(
- klass=self.__class__.__name__,
- year=self.year,
- month=self.month,
- day=self.day,
- )
- )
-
- # COMPARISONS
-
- def closest(self, dt1, dt2):
- """
- Get the closest date from the instance.
-
- :type dt1: Date or date
- :type dt2: Date or date
-
- :rtype: Date
- """
- dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
- dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
-
- if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
- return dt1
-
- return dt2
-
- def farthest(self, dt1, dt2):
- """
- Get the farthest date from the instance.
-
- :type dt1: Date or date
- :type dt2: Date or date
-
- :rtype: Date
- """
- dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
- dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
-
- if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
- return dt1
-
- return dt2
-
- def is_future(self):
- """
- Determines if the instance is in the future, ie. greater than now.
-
- :rtype: bool
- """
- return self > self.today()
-
- def is_past(self):
- """
- Determines if the instance is in the past, ie. less than now.
-
- :rtype: bool
- """
- return self < self.today()
-
- def is_leap_year(self):
- """
- Determines if the instance is a leap year.
-
- :rtype: bool
- """
- return calendar.isleap(self.year)
-
- def is_long_year(self):
- """
- Determines if the instance is a long year
-
- See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_
-
- :rtype: bool
- """
- return Date(self.year, 12, 28).isocalendar()[1] == 53
-
- def is_same_day(self, dt):
- """
- Checks if the passed in date is the same day as the instance current day.
-
- :type dt: Date or date
-
- :rtype: bool
- """
- return self == dt
-
- def is_anniversary(self, dt=None):
- """
- Check if its the anniversary.
-
- Compares the date/month values of the two dates.
-
- :rtype: bool
- """
- if dt is None:
- dt = Date.today()
-
- instance = self.__class__(dt.year, dt.month, dt.day)
-
- return (self.month, self.day) == (instance.month, instance.day)
-
- # the additional method for checking if today is the anniversary day
- # the alias is provided to start using a new name and keep the backward compatibility
- # the old name can be completely replaced with the new in one of the future versions
- is_birthday = is_anniversary
-
- # ADDITIONS AND SUBSTRACTIONS
-
- def add(self, years=0, months=0, weeks=0, days=0):
- """
- Add duration to the instance.
-
- :param years: The number of years
- :type years: int
-
- :param months: The number of months
- :type months: int
-
- :param weeks: The number of weeks
- :type weeks: int
-
- :param days: The number of days
- :type days: int
-
- :rtype: Date
- """
- dt = add_duration(
- date(self.year, self.month, self.day),
- years=years,
- months=months,
- weeks=weeks,
- days=days,
- )
-
- return self.__class__(dt.year, dt.month, dt.day)
-
- def subtract(self, years=0, months=0, weeks=0, days=0):
- """
- Remove duration from the instance.
-
- :param years: The number of years
- :type years: int
-
- :param months: The number of months
- :type months: int
-
- :param weeks: The number of weeks
- :type weeks: int
-
- :param days: The number of days
- :type days: int
-
- :rtype: Date
- """
- return self.add(years=-years, months=-months, weeks=-weeks, days=-days)
-
- def _add_timedelta(self, delta):
- """
- Add timedelta duration to the instance.
-
- :param delta: The timedelta instance
- :type delta: pendulum.Duration or datetime.timedelta
-
- :rtype: Date
- """
- if isinstance(delta, pendulum.Duration):
- return self.add(
- years=delta.years,
- months=delta.months,
- weeks=delta.weeks,
- days=delta.remaining_days,
- )
-
- return self.add(days=delta.days)
-
- def _subtract_timedelta(self, delta):
- """
- Remove timedelta duration from the instance.
-
- :param delta: The timedelta instance
- :type delta: pendulum.Duration or datetime.timedelta
-
- :rtype: Date
- """
- if isinstance(delta, pendulum.Duration):
- return self.subtract(
- years=delta.years,
- months=delta.months,
- weeks=delta.weeks,
- days=delta.remaining_days,
- )
-
- return self.subtract(days=delta.days)
-
- def __add__(self, other):
- if not isinstance(other, timedelta):
- return NotImplemented
-
- return self._add_timedelta(other)
-
- def __sub__(self, other):
- if isinstance(other, timedelta):
- return self._subtract_timedelta(other)
-
- if not isinstance(other, date):
- return NotImplemented
-
- dt = self.__class__(other.year, other.month, other.day)
-
- return dt.diff(self, False)
-
- # DIFFERENCES
-
- def diff(self, dt=None, abs=True):
- """
- Returns the difference between two Date objects as a Period.
-
- :type dt: Date or None
-
- :param abs: Whether to return an absolute interval or not
- :type abs: bool
-
- :rtype: Period
- """
- if dt is None:
- dt = self.today()
-
- return Period(self, Date(dt.year, dt.month, dt.day), absolute=abs)
-
- def diff_for_humans(self, other=None, absolute=False, locale=None):
- """
- Get the difference in a human readable format in the current locale.
-
- When comparing a value in the past to default now:
- 1 day ago
- 5 months ago
-
- When comparing a value in the future to default now:
- 1 day from now
- 5 months from now
-
- When comparing a value in the past to another value:
- 1 day before
- 5 months before
-
- When comparing a value in the future to another value:
- 1 day after
- 5 months after
-
- :type other: Date
-
- :param absolute: removes time difference modifiers ago, after, etc
- :type absolute: bool
-
- :param locale: The locale to use for localization
- :type locale: str
-
- :rtype: str
- """
- is_now = other is None
-
- if is_now:
- other = self.today()
-
- diff = self.diff(other)
-
- return pendulum.format_diff(diff, is_now, absolute, locale)
-
- # MODIFIERS
-
- def start_of(self, unit):
- """
- Returns a copy of the instance with the time reset
- with the following rules:
-
- * day: time to 00:00:00
- * week: date to first day of the week and time to 00:00:00
- * month: date to first day of the month and time to 00:00:00
- * year: date to first day of the year and time to 00:00:00
- * decade: date to first day of the decade and time to 00:00:00
- * century: date to first day of century and time to 00:00:00
-
- :param unit: The unit to reset to
- :type unit: str
-
- :rtype: Date
- """
- if unit not in self._MODIFIERS_VALID_UNITS:
- raise ValueError('Invalid unit "{}" for start_of()'.format(unit))
-
- return getattr(self, "_start_of_{}".format(unit))()
-
- def end_of(self, unit):
- """
- Returns a copy of the instance with the time reset
- with the following rules:
-
- * week: date to last day of the week
- * month: date to last day of the month
- * year: date to last day of the year
- * decade: date to last day of the decade
- * century: date to last day of century
-
- :param unit: The unit to reset to
- :type unit: str
-
- :rtype: Date
- """
- if unit not in self._MODIFIERS_VALID_UNITS:
- raise ValueError('Invalid unit "%s" for end_of()' % unit)
-
- return getattr(self, "_end_of_%s" % unit)()
-
- def _start_of_day(self):
- """
- Compatibility method.
-
- :rtype: Date
- """
- return self
-
- def _end_of_day(self):
- """
- Compatibility method
-
- :rtype: Date
- """
- return self
-
- def _start_of_month(self):
- """
- Reset the date to the first day of the month.
-
- :rtype: Date
- """
- return self.set(self.year, self.month, 1)
-
- def _end_of_month(self):
- """
- Reset the date to the last day of the month.
-
- :rtype: Date
- """
- return self.set(self.year, self.month, self.days_in_month)
-
- def _start_of_year(self):
- """
- Reset the date to the first day of the year.
-
- :rtype: Date
- """
- return self.set(self.year, 1, 1)
-
- def _end_of_year(self):
- """
- Reset the date to the last day of the year.
-
- :rtype: Date
- """
- return self.set(self.year, 12, 31)
-
- def _start_of_decade(self):
- """
- Reset the date to the first day of the decade.
-
- :rtype: Date
- """
- year = self.year - self.year % YEARS_PER_DECADE
-
- return self.set(year, 1, 1)
-
- def _end_of_decade(self):
- """
- Reset the date to the last day of the decade.
-
- :rtype: Date
- """
- year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
-
- return self.set(year, 12, 31)
-
- def _start_of_century(self):
- """
- Reset the date to the first day of the century.
-
- :rtype: Date
- """
- year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
-
- return self.set(year, 1, 1)
-
- def _end_of_century(self):
- """
- Reset the date to the last day of the century.
-
- :rtype: Date
- """
- year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
-
- return self.set(year, 12, 31)
-
- def _start_of_week(self):
- """
- Reset the date to the first day of the week.
-
- :rtype: Date
- """
- dt = self
-
- if self.day_of_week != pendulum._WEEK_STARTS_AT:
- dt = self.previous(pendulum._WEEK_STARTS_AT)
-
- return dt.start_of("day")
-
- def _end_of_week(self):
- """
- Reset the date to the last day of the week.
-
- :rtype: Date
- """
- dt = self
-
- if self.day_of_week != pendulum._WEEK_ENDS_AT:
- dt = self.next(pendulum._WEEK_ENDS_AT)
-
- return dt.end_of("day")
-
- def next(self, day_of_week=None):
- """
- Modify to the next occurrence of a given day of the week.
- If no day_of_week is provided, modify to the next occurrence
- of the current day of the week. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :param day_of_week: The next day of week to reset to.
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if day_of_week is None:
- day_of_week = self.day_of_week
-
- if day_of_week < SUNDAY or day_of_week > SATURDAY:
- raise ValueError("Invalid day of week")
-
- dt = self.add(days=1)
- while dt.day_of_week != day_of_week:
- dt = dt.add(days=1)
-
- return dt
-
- def previous(self, day_of_week=None):
- """
- Modify to the previous occurrence of a given day of the week.
- If no day_of_week is provided, modify to the previous occurrence
- of the current day of the week. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :param day_of_week: The previous day of week to reset to.
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if day_of_week is None:
- day_of_week = self.day_of_week
-
- if day_of_week < SUNDAY or day_of_week > SATURDAY:
- raise ValueError("Invalid day of week")
-
- dt = self.subtract(days=1)
- while dt.day_of_week != day_of_week:
- dt = dt.subtract(days=1)
-
- return dt
-
- def first_of(self, unit, day_of_week=None):
- """
- Returns an instance set to the first occurrence
- of a given day of the week in the current unit.
- If no day_of_week is provided, modify to the first day of the unit.
- Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- Supported units are month, quarter and year.
-
- :param unit: The unit to use
- :type unit: str
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if unit not in ["month", "quarter", "year"]:
- raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
-
- return getattr(self, "_first_of_{}".format(unit))(day_of_week)
-
- def last_of(self, unit, day_of_week=None):
- """
- Returns an instance set to the last occurrence
- of a given day of the week in the current unit.
- If no day_of_week is provided, modify to the last day of the unit.
- Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- Supported units are month, quarter and year.
-
- :param unit: The unit to use
- :type unit: str
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if unit not in ["month", "quarter", "year"]:
- raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
-
- return getattr(self, "_last_of_{}".format(unit))(day_of_week)
-
- def nth_of(self, unit, nth, day_of_week):
- """
- Returns a new instance set to the given occurrence
- of a given day of the week in the current unit.
- If the calculated occurrence is outside the scope of the current unit,
- then raise an error. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- Supported units are month, quarter and year.
-
- :param unit: The unit to use
- :type unit: str
-
- :type nth: int
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if unit not in ["month", "quarter", "year"]:
- raise ValueError('Invalid unit "{}" for first_of()'.format(unit))
-
- dt = getattr(self, "_nth_of_{}".format(unit))(nth, day_of_week)
- if dt is False:
- raise PendulumException(
- "Unable to find occurence {} of {} in {}".format(
- nth, self._days[day_of_week], unit
- )
- )
-
- return dt
-
- def _first_of_month(self, day_of_week):
- """
- Modify to the first occurrence of a given day of the week
- in the current month. If no day_of_week is provided,
- modify to the first day of the month. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int
-
- :rtype: Date
- """
- dt = self
-
- if day_of_week is None:
- return dt.set(day=1)
-
- month = calendar.monthcalendar(dt.year, dt.month)
-
- calendar_day = (day_of_week - 1) % 7
-
- if month[0][calendar_day] > 0:
- day_of_month = month[0][calendar_day]
- else:
- day_of_month = month[1][calendar_day]
-
- return dt.set(day=day_of_month)
-
- def _last_of_month(self, day_of_week=None):
- """
- Modify to the last occurrence of a given day of the week
- in the current month. If no day_of_week is provided,
- modify to the last day of the month. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- dt = self
-
- if day_of_week is None:
- return dt.set(day=self.days_in_month)
-
- month = calendar.monthcalendar(dt.year, dt.month)
-
- calendar_day = (day_of_week - 1) % 7
-
- if month[-1][calendar_day] > 0:
- day_of_month = month[-1][calendar_day]
- else:
- day_of_month = month[-2][calendar_day]
-
- return dt.set(day=day_of_month)
-
- def _nth_of_month(self, nth, day_of_week):
- """
- Modify to the given occurrence of a given day of the week
- in the current month. If the calculated occurrence is outside,
- the scope of the current month, then return False and no
- modifications are made. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type nth: int
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if nth == 1:
- return self.first_of("month", day_of_week)
-
- dt = self.first_of("month")
- check = dt.format("YYYY-MM")
- for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
- dt = dt.next(day_of_week)
-
- if dt.format("YYYY-MM") == check:
- return self.set(day=dt.day)
-
- return False
-
- def _first_of_quarter(self, day_of_week=None):
- """
- Modify to the first occurrence of a given day of the week
- in the current quarter. If no day_of_week is provided,
- modify to the first day of the quarter. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- return self.set(self.year, self.quarter * 3 - 2, 1).first_of(
- "month", day_of_week
- )
-
- def _last_of_quarter(self, day_of_week=None):
- """
- Modify to the last occurrence of a given day of the week
- in the current quarter. If no day_of_week is provided,
- modify to the last day of the quarter. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
-
- def _nth_of_quarter(self, nth, day_of_week):
- """
- Modify to the given occurrence of a given day of the week
- in the current quarter. If the calculated occurrence is outside,
- the scope of the current quarter, then return False and no
- modifications are made. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type nth: int
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if nth == 1:
- return self.first_of("quarter", day_of_week)
-
- dt = self.replace(self.year, self.quarter * 3, 1)
- last_month = dt.month
- year = dt.year
- dt = dt.first_of("quarter")
- for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
- dt = dt.next(day_of_week)
-
- if last_month < dt.month or year != dt.year:
- return False
-
- return self.set(self.year, dt.month, dt.day)
-
- def _first_of_year(self, day_of_week=None):
- """
- Modify to the first occurrence of a given day of the week
- in the current year. If no day_of_week is provided,
- modify to the first day of the year. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- return self.set(month=1).first_of("month", day_of_week)
-
- def _last_of_year(self, day_of_week=None):
- """
- Modify to the last occurrence of a given day of the week
- in the current year. If no day_of_week is provided,
- modify to the last day of the year. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
-
- def _nth_of_year(self, nth, day_of_week):
- """
- Modify to the given occurrence of a given day of the week
- in the current year. If the calculated occurrence is outside,
- the scope of the current year, then return False and no
- modifications are made. Use the supplied consts
- to indicate the desired day_of_week, ex. pendulum.MONDAY.
-
- :type nth: int
-
- :type day_of_week: int or None
-
- :rtype: Date
- """
- if nth == 1:
- return self.first_of("year", day_of_week)
-
- dt = self.first_of("year")
- year = dt.year
- for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
- dt = dt.next(day_of_week)
-
- if year != dt.year:
- return False
-
- return self.set(self.year, dt.month, dt.day)
-
- def average(self, dt=None):
- """
- Modify the current instance to the average
- of a given instance (default now) and the current instance.
-
- :type dt: Date or date
-
- :rtype: Date
- """
- if dt is None:
- dt = Date.today()
-
- return self.add(days=int(self.diff(dt, False).in_days() / 2))
-
- # Native methods override
-
- @classmethod
- def today(cls):
- return pendulum.today().date()
-
- @classmethod
- def fromtimestamp(cls, t):
- dt = super(Date, cls).fromtimestamp(t)
-
- return cls(dt.year, dt.month, dt.day)
-
- @classmethod
- def fromordinal(cls, n):
- dt = super(Date, cls).fromordinal(n)
-
- return cls(dt.year, dt.month, dt.day)
-
- def replace(self, year=None, month=None, day=None):
- year = year if year is not None else self.year
- month = month if month is not None else self.month
- day = day if day is not None else self.day
-
- return self.__class__(year, month, day)
+from __future__ import annotations
+
+import calendar
+import math
+
+from datetime import date
+from datetime import datetime
+from datetime import timedelta
+from typing import NoReturn
+from typing import cast
+from typing import overload
+
+import pendulum
+
+from pendulum.constants import FRIDAY
+from pendulum.constants import MONDAY
+from pendulum.constants import MONTHS_PER_YEAR
+from pendulum.constants import SATURDAY
+from pendulum.constants import SUNDAY
+from pendulum.constants import THURSDAY
+from pendulum.constants import TUESDAY
+from pendulum.constants import WEDNESDAY
+from pendulum.constants import YEARS_PER_CENTURY
+from pendulum.constants import YEARS_PER_DECADE
+from pendulum.exceptions import PendulumException
+from pendulum.helpers import add_duration
+from pendulum.interval import Interval
+from pendulum.mixins.default import FormattableMixin
+
+
+class Date(FormattableMixin, date):
+ # Names of days of the week
+ _days = {
+ SUNDAY: "Sunday",
+ MONDAY: "Monday",
+ TUESDAY: "Tuesday",
+ WEDNESDAY: "Wednesday",
+ THURSDAY: "Thursday",
+ FRIDAY: "Friday",
+ SATURDAY: "Saturday",
+ }
+
+ _MODIFIERS_VALID_UNITS = ["day", "week", "month", "year", "decade", "century"]
+
+ # Getters/Setters
+
+ def set(
+ self, year: int | None = None, month: int | None = None, day: int | None = None
+ ) -> Date:
+ return self.replace(year=year, month=month, day=day)
+
+ @property
+ def day_of_week(self) -> int:
+ """
+ Returns the day of the week (0-6).
+ """
+ return self.isoweekday() % 7
+
+ @property
+ def day_of_year(self) -> int:
+ """
+ Returns the day of the year (1-366).
+ """
+ k = 1 if self.is_leap_year() else 2
+
+ return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30
+
+ @property
+ def week_of_year(self) -> int:
+ return self.isocalendar()[1]
+
+ @property
+ def days_in_month(self) -> int:
+ return calendar.monthrange(self.year, self.month)[1]
+
+ @property
+ def week_of_month(self) -> int:
+ first_day_of_month = self.replace(day=1)
+
+ return self.week_of_year - first_day_of_month.week_of_year + 1
+
+ @property
+ def age(self) -> int:
+ return self.diff(abs=False).in_years()
+
+ @property
+ def quarter(self) -> int:
+ return int(math.ceil(self.month / 3))
+
+ # String Formatting
+
+ def to_date_string(self) -> str:
+ """
+ Format the instance as date.
+
+ :rtype: str
+ """
+ return self.strftime("%Y-%m-%d")
+
+ def to_formatted_date_string(self) -> str:
+ """
+ Format the instance as a readable date.
+
+ :rtype: str
+ """
+ return self.strftime("%b %d, %Y")
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self.year}, {self.month}, {self.day})"
+
+ # COMPARISONS
+
+ def closest(self, dt1: date, dt2: date) -> Date:
+ """
+ Get the closest date from the instance.
+ """
+ dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
+ dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
+
+ if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
+ return dt1
+
+ return dt2
+
+ def farthest(self, dt1: date, dt2: date) -> Date:
+ """
+ Get the farthest date from the instance.
+ """
+ dt1 = self.__class__(dt1.year, dt1.month, dt1.day)
+ dt2 = self.__class__(dt2.year, dt2.month, dt2.day)
+
+ if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
+ return dt1
+
+ return dt2
+
+ def is_future(self) -> bool:
+ """
+ Determines if the instance is in the future, ie. greater than now.
+ """
+ return self > self.today()
+
+ def is_past(self) -> bool:
+ """
+ Determines if the instance is in the past, ie. less than now.
+ """
+ return self < self.today()
+
+ def is_leap_year(self) -> bool:
+ """
+ Determines if the instance is a leap year.
+ """
+ return calendar.isleap(self.year)
+
+ def is_long_year(self) -> bool:
+ """
+ Determines if the instance is a long year
+
+ See link `<https://en.wikipedia.org/wiki/ISO_8601#Week_dates>`_
+ """
+ return Date(self.year, 12, 28).isocalendar()[1] == 53
+
+ def is_same_day(self, dt: date) -> bool:
+ """
+ Checks if the passed in date is the same day as the instance current day.
+ """
+ return self == dt
+
+ def is_anniversary(self, dt: date | None = None) -> bool:
+ """
+ Check if it's the anniversary.
+
+ Compares the date/month values of the two dates.
+ """
+ if dt is None:
+ dt = Date.today()
+
+ instance = self.__class__(dt.year, dt.month, dt.day)
+
+ return (self.month, self.day) == (instance.month, instance.day)
+
+ # the additional method for checking if today is the anniversary day
+ # the alias is provided to start using a new name and keep the backward compatibility
+ # the old name can be completely replaced with the new in one of the future versions
+ is_birthday = is_anniversary
+
+ # ADDITIONS AND SUBTRACTIONS
+
+ def add(
+ self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
+ ) -> Date:
+ """
+ Add duration to the instance.
+
+ :param years: The number of years
+ :param months: The number of months
+ :param weeks: The number of weeks
+ :param days: The number of days
+ """
+ dt = add_duration(
+ date(self.year, self.month, self.day),
+ years=years,
+ months=months,
+ weeks=weeks,
+ days=days,
+ )
+
+ return self.__class__(dt.year, dt.month, dt.day)
+
+ def subtract(
+ self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0
+ ) -> Date:
+ """
+ Remove duration from the instance.
+
+ :param years: The number of years
+ :param months: The number of months
+ :param weeks: The number of weeks
+ :param days: The number of days
+ """
+ return self.add(years=-years, months=-months, weeks=-weeks, days=-days)
+
+ def _add_timedelta(self, delta: timedelta) -> Date:
+ """
+ Add timedelta duration to the instance.
+
+ :param delta: The timedelta instance
+ """
+ if isinstance(delta, pendulum.Duration):
+ return self.add(
+ years=delta.years,
+ months=delta.months,
+ weeks=delta.weeks,
+ days=delta.remaining_days,
+ )
+
+ return self.add(days=delta.days)
+
+ def _subtract_timedelta(self, delta: timedelta) -> Date:
+ """
+ Remove timedelta duration from the instance.
+
+ :param delta: The timedelta instance
+ """
+ if isinstance(delta, pendulum.Duration):
+ return self.subtract(
+ years=delta.years,
+ months=delta.months,
+ weeks=delta.weeks,
+ days=delta.remaining_days,
+ )
+
+ return self.subtract(days=delta.days)
+
+ def __add__(self, other: timedelta) -> Date:
+ if not isinstance(other, timedelta):
+ return NotImplemented
+
+ return self._add_timedelta(other)
+
+ @overload
+ def __sub__(self, delta: timedelta) -> Date:
+ ...
+
+ @overload
+ def __sub__(self, dt: datetime) -> NoReturn:
+ ...
+
+ @overload
+ def __sub__(self, dt: Date) -> Interval:
+ ...
+
+ def __sub__(self, other: timedelta | date) -> Date | Interval:
+ if isinstance(other, timedelta):
+ return self._subtract_timedelta(other)
+
+ if not isinstance(other, date):
+ return NotImplemented
+
+ dt = self.__class__(other.year, other.month, other.day)
+
+ return dt.diff(self, False)
+
+ # DIFFERENCES
+
+ def diff(self, dt: date | None = None, abs: bool = True) -> Interval:
+ """
+ Returns the difference between two Date objects as an Interval.
+
+ :param dt: The date to compare to (defaults to today)
+ :param abs: Whether to return an absolute interval or not
+ """
+ if dt is None:
+ dt = self.today()
+
+ return Interval(self, Date(dt.year, dt.month, dt.day), absolute=abs)
+
+ def diff_for_humans(
+ self,
+ other: date | None = None,
+ absolute: bool = False,
+ locale: str | None = None,
+ ) -> str:
+ """
+ Get the difference in a human readable format in the current locale.
+
+ When comparing a value in the past to default now:
+ 1 day ago
+ 5 months ago
+
+ When comparing a value in the future to default now:
+ 1 day from now
+ 5 months from now
+
+ When comparing a value in the past to another value:
+ 1 day before
+ 5 months before
+
+ When comparing a value in the future to another value:
+ 1 day after
+ 5 months after
+
+ :param other: The date to compare to (defaults to today)
+ :param absolute: removes time difference modifiers ago, after, etc
+ :param locale: The locale to use for localization
+ """
+ is_now = other is None
+
+ if is_now:
+ other = self.today()
+
+ diff = self.diff(other)
+
+ return pendulum.format_diff(diff, is_now, absolute, locale)
+
+ # MODIFIERS
+
+ def start_of(self, unit: str) -> Date:
+ """
+ Returns a copy of the instance with the time reset
+ with the following rules:
+
+ * day: time to 00:00:00
+ * week: date to first day of the week and time to 00:00:00
+ * month: date to first day of the month and time to 00:00:00
+ * year: date to first day of the year and time to 00:00:00
+ * decade: date to first day of the decade and time to 00:00:00
+ * century: date to first day of century and time to 00:00:00
+
+ :param unit: The unit to reset to
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError(f'Invalid unit "{unit}" for start_of()')
+
+ return cast(Date, getattr(self, f"_start_of_{unit}")())
+
+ def end_of(self, unit: str) -> Date:
+ """
+ Returns a copy of the instance with the time reset
+ with the following rules:
+
+ * week: date to last day of the week
+ * month: date to last day of the month
+ * year: date to last day of the year
+ * decade: date to last day of the decade
+ * century: date to last day of century
+
+ :param unit: The unit to reset to
+ """
+ if unit not in self._MODIFIERS_VALID_UNITS:
+ raise ValueError(f'Invalid unit "{unit}" for end_of()')
+
+ return cast(Date, getattr(self, f"_end_of_{unit}")())
+
+ def _start_of_day(self) -> Date:
+ """
+ Compatibility method.
+ """
+ return self
+
+ def _end_of_day(self) -> Date:
+ """
+ Compatibility method
+ """
+ return self
+
+ def _start_of_month(self) -> Date:
+ """
+ Reset the date to the first day of the month.
+ """
+ return self.set(self.year, self.month, 1)
+
+ def _end_of_month(self) -> Date:
+ """
+ Reset the date to the last day of the month.
+ """
+ return self.set(self.year, self.month, self.days_in_month)
+
+ def _start_of_year(self) -> Date:
+ """
+ Reset the date to the first day of the year.
+ """
+ return self.set(self.year, 1, 1)
+
+ def _end_of_year(self) -> Date:
+ """
+ Reset the date to the last day of the year.
+ """
+ return self.set(self.year, 12, 31)
+
+ def _start_of_decade(self) -> Date:
+ """
+ Reset the date to the first day of the decade.
+ """
+ year = self.year - self.year % YEARS_PER_DECADE
+
+ return self.set(year, 1, 1)
+
+ def _end_of_decade(self) -> Date:
+ """
+ Reset the date to the last day of the decade.
+ """
+ year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1
+
+ return self.set(year, 12, 31)
+
+ def _start_of_century(self) -> Date:
+ """
+ Reset the date to the first day of the century.
+ """
+ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1
+
+ return self.set(year, 1, 1)
+
+ def _end_of_century(self) -> Date:
+ """
+ Reset the date to the last day of the century.
+ """
+ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY
+
+ return self.set(year, 12, 31)
+
+ def _start_of_week(self) -> Date:
+ """
+ Reset the date to the first day of the week.
+ """
+ dt = self
+
+ if self.day_of_week != pendulum._WEEK_STARTS_AT:
+ dt = self.previous(pendulum._WEEK_STARTS_AT)
+
+ return dt.start_of("day")
+
+ def _end_of_week(self) -> Date:
+ """
+ Reset the date to the last day of the week.
+ """
+ dt = self
+
+ if self.day_of_week != pendulum._WEEK_ENDS_AT:
+ dt = self.next(pendulum._WEEK_ENDS_AT)
+
+ return dt.end_of("day")
+
+ def next(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the next occurrence of a given day of the week.
+ If no day_of_week is provided, modify to the next occurrence
+ of the current day of the week. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ :param day_of_week: The next day of week to reset to.
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < SUNDAY or day_of_week > SATURDAY:
+ raise ValueError("Invalid day of week")
+
+ dt = self.add(days=1)
+ while dt.day_of_week != day_of_week:
+ dt = dt.add(days=1)
+
+ return dt
+
+ def previous(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the previous occurrence of a given day of the week.
+ If no day_of_week is provided, modify to the previous occurrence
+ of the current day of the week. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ :param day_of_week: The previous day of week to reset to.
+ """
+ if day_of_week is None:
+ day_of_week = self.day_of_week
+
+ if day_of_week < SUNDAY or day_of_week > SATURDAY:
+ raise ValueError("Invalid day of week")
+
+ dt = self.subtract(days=1)
+ while dt.day_of_week != day_of_week:
+ dt = dt.subtract(days=1)
+
+ return dt
+
+ def first_of(self, unit: str, day_of_week: int | None = None) -> Date:
+ """
+ Returns an instance set to the first occurrence
+ of a given day of the week in the current unit.
+ If no day_of_week is provided, modify to the first day of the unit.
+ Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ Supported units are month, quarter and year.
+
+ :param unit: The unit to use
+ :param day_of_week: The day of week to reset to.
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError(f'Invalid unit "{unit}" for first_of()')
+
+ return cast(Date, getattr(self, f"_first_of_{unit}")(day_of_week))
+
+ def last_of(self, unit: str, day_of_week: int | None = None) -> Date:
+ """
+ Returns an instance set to the last occurrence
+ of a given day of the week in the current unit.
+ If no day_of_week is provided, modify to the last day of the unit.
+ Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ Supported units are month, quarter and year.
+
+ :param unit: The unit to use
+ :param day_of_week: The day of week to reset to.
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError(f'Invalid unit "{unit}" for first_of()')
+
+ return cast(Date, getattr(self, f"_last_of_{unit}")(day_of_week))
+
+ def nth_of(self, unit: str, nth: int, day_of_week: int) -> Date:
+ """
+ Returns a new instance set to the given occurrence
+ of a given day of the week in the current unit.
+ If the calculated occurrence is outside the scope of the current unit,
+ then raise an error. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ Supported units are month, quarter and year.
+
+ :param unit: The unit to use
+ :param nth: The occurrence to use
+ :param day_of_week: The day of week to set to.
+ """
+ if unit not in ["month", "quarter", "year"]:
+ raise ValueError(f'Invalid unit "{unit}" for first_of()')
+
+ dt = cast(Date, getattr(self, f"_nth_of_{unit}")(nth, day_of_week))
+ if not dt:
+ raise PendulumException(
+ f"Unable to find occurence {nth}"
+ f" of {self._days[day_of_week]} in {unit}"
+ )
+
+ return dt
+
+ def _first_of_month(self, day_of_week: int) -> Date:
+ """
+ Modify to the first occurrence of a given day of the week
+ in the current month. If no day_of_week is provided,
+ modify to the first day of the month. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ :param day_of_week: The day of week to set to.
+ """
+ dt = self
+
+ if day_of_week is None:
+ return dt.set(day=1)
+
+ month = calendar.monthcalendar(dt.year, dt.month)
+
+ calendar_day = (day_of_week - 1) % 7
+
+ if month[0][calendar_day] > 0:
+ day_of_month = month[0][calendar_day]
+ else:
+ day_of_month = month[1][calendar_day]
+
+ return dt.set(day=day_of_month)
+
+ def _last_of_month(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the last occurrence of a given day of the week
+ in the current month. If no day_of_week is provided,
+ modify to the last day of the month. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+
+ :param day_of_week: The day of week to set to.
+ """
+ dt = self
+
+ if day_of_week is None:
+ return dt.set(day=self.days_in_month)
+
+ month = calendar.monthcalendar(dt.year, dt.month)
+
+ calendar_day = (day_of_week - 1) % 7
+
+ if month[-1][calendar_day] > 0:
+ day_of_month = month[-1][calendar_day]
+ else:
+ day_of_month = month[-2][calendar_day]
+
+ return dt.set(day=day_of_month)
+
+ def _nth_of_month(self, nth: int, day_of_week: int) -> Date | None:
+ """
+ Modify to the given occurrence of a given day of the week
+ in the current month. If the calculated occurrence is outside,
+ the scope of the current month, then return False and no
+ modifications are made. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ if nth == 1:
+ return self.first_of("month", day_of_week)
+
+ dt = self.first_of("month")
+ check = dt.format("YYYY-MM")
+ for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
+ dt = dt.next(day_of_week)
+
+ if dt.format("YYYY-MM") == check:
+ return self.set(day=dt.day)
+
+ return None
+
+ def _first_of_quarter(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the first occurrence of a given day of the week
+ in the current quarter. If no day_of_week is provided,
+ modify to the first day of the quarter. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ return self.set(self.year, self.quarter * 3 - 2, 1).first_of(
+ "month", day_of_week
+ )
+
+ def _last_of_quarter(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the last occurrence of a given day of the week
+ in the current quarter. If no day_of_week is provided,
+ modify to the last day of the quarter. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week)
+
+ def _nth_of_quarter(self, nth: int, day_of_week: int) -> Date | None:
+ """
+ Modify to the given occurrence of a given day of the week
+ in the current quarter. If the calculated occurrence is outside,
+ the scope of the current quarter, then return False and no
+ modifications are made. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ if nth == 1:
+ return self.first_of("quarter", day_of_week)
+
+ dt = self.replace(self.year, self.quarter * 3, 1)
+ last_month = dt.month
+ year = dt.year
+ dt = dt.first_of("quarter")
+ for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
+ dt = dt.next(day_of_week)
+
+ if last_month < dt.month or year != dt.year:
+ return None
+
+ return self.set(self.year, dt.month, dt.day)
+
+ def _first_of_year(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the first occurrence of a given day of the week
+ in the current year. If no day_of_week is provided,
+ modify to the first day of the year. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ return self.set(month=1).first_of("month", day_of_week)
+
+ def _last_of_year(self, day_of_week: int | None = None) -> Date:
+ """
+ Modify to the last occurrence of a given day of the week
+ in the current year. If no day_of_week is provided,
+ modify to the last day of the year. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week)
+
+ def _nth_of_year(self, nth: int, day_of_week: int) -> Date | None:
+ """
+ Modify to the given occurrence of a given day of the week
+ in the current year. If the calculated occurrence is outside,
+ the scope of the current year, then return False and no
+ modifications are made. Use the supplied consts
+ to indicate the desired day_of_week, ex. pendulum.MONDAY.
+ """
+ if nth == 1:
+ return self.first_of("year", day_of_week)
+
+ dt = self.first_of("year")
+ year = dt.year
+ for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)):
+ dt = dt.next(day_of_week)
+
+ if year != dt.year:
+ return None
+
+ return self.set(self.year, dt.month, dt.day)
+
+ def average(self, dt: date | None = None) -> Date:
+ """
+ Modify the current instance to the average
+ of a given instance (default now) and the current instance.
+ """
+ if dt is None:
+ dt = Date.today()
+
+ return self.add(days=int(self.diff(dt, False).in_days() / 2))
+
+ # Native methods override
+
+ @classmethod
+ def today(cls) -> Date:
+ dt = date.today()
+
+ return cls(dt.year, dt.month, dt.day)
+
+ @classmethod
+ def fromtimestamp(cls, t: float) -> Date:
+ dt = super().fromtimestamp(t)
+
+ return cls(dt.year, dt.month, dt.day)
+
+ @classmethod
+ def fromordinal(cls, n: int) -> Date:
+ dt = super().fromordinal(n)
+
+ return cls(dt.year, dt.month, dt.day)
+
+ def replace(
+ self,
+ year: int | None = None,
+ month: int | None = None,
+ day: int | None = None,
+ ) -> Date:
+ year = year if year is not None else self.year
+ month = month if month is not None else self.month
+ day = day if day is not None else self.day
+
+ return self.__class__(year, month, day)