"""Utilities for managing the fiscal calendar."""
import calendar
import contextlib
import datetime
from typing import Iterator, Optional, Union, cast
__author__ = "Adam J. Stewart"
__version__ = "0.4.0"
# Number of months in each quarter
MONTHS_PER_QUARTER = 12 // 4
MIN_QUARTER = 1
MAX_QUARTER = 4
# These global variables control the start of the fiscal year.
# The default is to use the U.S. federal government's fiscal year,
# but they can be changed to use any other fiscal year.
START_YEAR = "previous"
START_MONTH = 10
START_DAY = 1
def _validate_fiscal_calendar_params(
start_year: str, start_month: int, start_day: int
) -> None:
"""Raise an Exception if the calendar parameters are invalid.
:param start_year: Relationship between the start of the fiscal year and
the calendar year. Possible values: ``'previous'`` or ``'same'``.
:param start_month: The first month of the fiscal year
:param start_day: The first day of the first month of the fiscal year
:raises ValueError: If ``start_year`` is not ``'previous'`` or ``'same'``
:raises ValueError: If ``start_month`` or ``start_day`` is out of range
"""
if start_year not in ["previous", "same"]:
msg = f"'start_year' must be either 'previous' or 'same', not: '{start_year}'"
raise ValueError(msg)
_check_day(start_month, start_day)
[docs]def setup_fiscal_calendar(
start_year: Optional[str] = None,
start_month: Optional[int] = None,
start_day: Optional[int] = None,
) -> None:
"""Modify the start of the fiscal calendar.
:param start_year: Relationship between the start of the fiscal year and
the calendar year. Possible values: ``'previous'`` or ``'same'``.
:param start_month: The first month of the fiscal year
:param start_day: The first day of the first month of the fiscal year
:raises ValueError: If ``start_year`` is not ``'previous'`` or ``'same'``
:raises ValueError: If ``start_month`` or ``start_day`` is out of range
"""
global START_YEAR, START_MONTH, START_DAY
# If arguments are omitted, use the currently active values.
start_year = START_YEAR if start_year is None else start_year
start_month = START_MONTH if start_month is None else start_month
start_day = START_DAY if start_day is None else start_day
_validate_fiscal_calendar_params(start_year, start_month, start_day)
START_YEAR = start_year
START_MONTH = start_month
START_DAY = start_day
[docs]@contextlib.contextmanager
def fiscal_calendar(
start_year: Optional[str] = None,
start_month: Optional[int] = None,
start_day: Optional[int] = None,
) -> Iterator[None]:
"""A context manager that lets you modify the start of the fiscal calendar
inside the scope of a with-statement.
:param start_year: Relationship between the start of the fiscal year and
the calendar year. Possible values: ``'previous'`` or ``'same'``.
:param start_month: The first month of the fiscal year
:param start_day: The first day of the first month of the fiscal year
:raises ValueError: If ``start_year`` is not ``'previous'`` or ``'same'``
:raises ValueError: If ``start_month`` or ``start_day`` is out of range
"""
# If arguments are omitted, use the currently active values.
start_year = START_YEAR if start_year is None else start_year
start_month = START_MONTH if start_month is None else start_month
start_day = START_DAY if start_day is None else start_day
# Temporarily change global variables
previous_values = (START_YEAR, START_MONTH, START_DAY)
setup_fiscal_calendar(start_year, start_month, start_day)
yield
# Restore previous values
setup_fiscal_calendar(*previous_values)
def _check_year(year: int) -> int:
"""Check if ``year`` is a valid year.
:param year: The year to test
:return: The year
:raises ValueError: If ``year`` is out of range
"""
if datetime.MINYEAR <= year <= datetime.MAXYEAR:
return year
else:
raise ValueError(f"year {year} is out of range")
def _check_month(month: int) -> int:
"""Check if ``month`` is a valid month.
:param month: The month to test
:return: The month
:raises ValueError: If ``month`` is out of range
"""
if 1 <= month <= 12:
return month
else:
raise ValueError(f"month {month} is out of range")
def _check_day(month: int, day: int) -> int:
"""Check if ``day`` is a valid day of month.
:param month: The month to test
:param day: The day to test
:return: The day
:raises ValueError: If ``month`` or ``day`` is out of range
"""
month = _check_month(month)
# Find the last day of the month
# Use a non-leap year
max_day = calendar.monthrange(2001, month)[1]
if 1 <= day <= max_day:
return day
else:
raise ValueError(f"day {day} is out of range")
def _check_fiscal_day(fiscal_year: int, fiscal_day: int) -> int:
"""Check if ``day`` is a valid day of the fiscal year.
:param fiscal_year: The fiscal year to test
:param fiscal_day: The fiscal day to test
:return: The fiscal day
:raises ValueError: If ``year`` or ``day`` is out of range
"""
fiscal_year = _check_year(fiscal_year)
# Find the length of the year
max_day = 366 if FiscalYear(fiscal_year).isleap else 365
if 1 <= fiscal_day <= max_day:
return fiscal_day
else:
raise ValueError(f"fiscal_day {fiscal_day} is out of range")
def _check_quarter(quarter: int) -> int:
"""Check if ``quarter`` is a valid quarter.
:param quarter: The quarter to test
:return: The quarter
:raises ValueError: If ``quarter`` is out of range
"""
if MIN_QUARTER <= quarter <= MAX_QUARTER:
return quarter
else:
raise ValueError(f"quarter {quarter} is out of range")
class _Hashable:
"""A class to make Fiscal objects hashable"""
def __hash__(self) -> int:
"""Unique hash of an object instance based on __slots__
:returns: a unique hash
"""
return hash(tuple(map(lambda x: getattr(self, x), self.__slots__)))
[docs]class FiscalYear(_Hashable):
"""A class representing a single fiscal year."""
__slots__ = ["_fiscal_year"]
__hash__ = _Hashable.__hash__
_fiscal_year: int
def __new__(cls, fiscal_year: int) -> "FiscalYear":
"""Constructor.
:param fiscal_year: The fiscal year
:returns: A newly constructed FiscalYear object
:raises ValueError: If ``fiscal_year`` is out of range
"""
fiscal_year = _check_year(fiscal_year)
self = super(FiscalYear, cls).__new__(cls)
self._fiscal_year = fiscal_year
return self
[docs] @classmethod
def current(cls) -> "FiscalYear":
"""Alternative constructor. Returns the current FiscalYear.
:returns: A newly constructed FiscalYear object
"""
today = FiscalDate.today()
return cls(today.fiscal_year)
def __repr__(self) -> str:
"""Convert to formal string, for repr().
>>> fy = FiscalYear(2017)
>>> repr(fy)
'FiscalYear(2017)'
"""
return f"{self.__class__.__name__}({self._fiscal_year})"
def __str__(self) -> str:
"""Convert to informal string, for str().
>>> fy = FiscalYear(2017)
>>> str(fy)
'FY2017'
"""
return f"FY{self._fiscal_year}"
# TODO: Implement __format__ so that you can print
# fiscal year as 17 or 2017 (%y or %Y)
def __contains__(
self,
item: Union[
"FiscalYear",
"FiscalQuarter",
"FiscalMonth",
"FiscalDay",
datetime.datetime,
datetime.date,
],
) -> bool:
""":param item: The item to check
:returns: True if item in self, else False
"""
if isinstance(item, FiscalYear):
return self == item
elif isinstance(item, (FiscalQuarter, FiscalMonth, FiscalDay)):
return self._fiscal_year == item.fiscal_year
elif isinstance(item, datetime.datetime):
return self.start <= item <= self.end
else:
return self.start.date() <= item <= self.end.date()
# Read-only field accessors
@property
def fiscal_year(self) -> int:
""":returns: The fiscal year"""
return self._fiscal_year
@property
def prev_fiscal_year(self) -> "FiscalYear":
""":returns: The previous fiscal year"""
return FiscalYear(self._fiscal_year - 1)
@property
def next_fiscal_year(self) -> "FiscalYear":
""":returns: The next fiscal year"""
return FiscalYear(self._fiscal_year + 1)
@property
def start(self) -> "FiscalDateTime":
""":returns: Start of the fiscal year"""
return self.q1.start
@property
def end(self) -> "FiscalDateTime":
""":returns: End of the fiscal year"""
return self.q4.end
@property
def q1(self) -> "FiscalQuarter":
""":returns: The first quarter of the fiscal year"""
return FiscalQuarter(self._fiscal_year, 1)
@property
def q2(self) -> "FiscalQuarter":
""":returns: The second quarter of the fiscal year"""
return FiscalQuarter(self._fiscal_year, 2)
@property
def q3(self) -> "FiscalQuarter":
""":returns: The third quarter of the fiscal year"""
return FiscalQuarter(self._fiscal_year, 3)
@property
def q4(self) -> "FiscalQuarter":
""":returns: The fourth quarter of the fiscal year"""
return FiscalQuarter(self._fiscal_year, 4)
@property
def isleap(self) -> bool:
"""returns: True if the fiscal year contains a leap day, else False"""
fiscal_year = FiscalYear(self._fiscal_year)
starts_on_or_before_possible_leap_day = (
fiscal_year.start.month,
fiscal_year.start.day,
) < (3, 1)
if START_YEAR == "previous":
if starts_on_or_before_possible_leap_day:
calendar_year = self._fiscal_year - 1
else:
calendar_year = self._fiscal_year
elif START_YEAR == "same":
if starts_on_or_before_possible_leap_day:
calendar_year = self._fiscal_year
else:
calendar_year = self._fiscal_year + 1
return calendar.isleap(calendar_year)
# Comparisons of FiscalYear objects with other
def __lt__(self, other: "FiscalYear") -> bool:
return self._fiscal_year < other._fiscal_year
def __le__(self, other: "FiscalYear") -> bool:
return self._fiscal_year <= other._fiscal_year
def __eq__(self, other: object) -> bool:
if isinstance(other, FiscalYear):
return self._fiscal_year == other._fiscal_year
else:
raise TypeError(
f"can't compare '{type(self).__name__}' to '{type(other).__name__}'"
)
def __ne__(self, other: object) -> bool:
if isinstance(other, FiscalYear):
return self._fiscal_year != other._fiscal_year
else:
raise TypeError(
f"can't compare '{type(self).__name__}' to '{type(other).__name__}'"
)
def __gt__(self, other: "FiscalYear") -> bool:
return self._fiscal_year > other._fiscal_year
def __ge__(self, other: "FiscalYear") -> bool:
return self._fiscal_year >= other._fiscal_year
[docs]class FiscalQuarter(_Hashable):
"""A class representing a single fiscal quarter."""
__slots__ = ["_fiscal_year", "_fiscal_quarter"]
__hash__ = _Hashable.__hash__
_fiscal_year: int
_fiscal_quarter: int
def __new__(cls, fiscal_year: int, fiscal_quarter: int) -> "FiscalQuarter":
"""Constructor.
:param fiscal_year: The fiscal year
:param fiscal_quarter: The fiscal quarter
:returns: A newly constructed FiscalQuarter object
:raises ValueError: If fiscal_year or fiscal_quarter is out of range
"""
fiscal_year = _check_year(fiscal_year)
fiscal_quarter = _check_quarter(fiscal_quarter)
self = super(FiscalQuarter, cls).__new__(cls)
self._fiscal_year = fiscal_year
self._fiscal_quarter = fiscal_quarter
return self
[docs] @classmethod
def current(cls) -> "FiscalQuarter":
"""Alternative constructor. Returns the current FiscalQuarter.
:returns: A newly constructed FiscalQuarter object
"""
today = FiscalDate.today()
return cls(today.fiscal_year, today.fiscal_quarter)
def __repr__(self) -> str:
"""Convert to formal string, for repr().
>>> q3 = FiscalQuarter(2017, 3)
>>> repr(q3)
'FiscalQuarter(2017, 3)'
"""
return f"{self.__class__.__name__}({self._fiscal_year}, {self._fiscal_quarter})"
def __str__(self) -> str:
"""Convert to informal string, for str().
>>> q3 = FiscalQuarter(2017, 3)
>>> str(q3)
'FY2017 Q3'
"""
return f"FY{self._fiscal_year} Q{self._fiscal_quarter}"
# TODO: Implement __format__ so that you can print
# fiscal year as 17 or 2017 (%y or %Y)
def __contains__(
self,
item: Union[
"FiscalQuarter",
"FiscalMonth",
"FiscalDay",
datetime.datetime,
datetime.date,
],
) -> bool:
"""Returns True if item in self, else False.
:param item: The item to check
"""
if isinstance(item, FiscalQuarter):
return self == item
elif isinstance(item, (FiscalMonth, FiscalDay)):
return self.start <= item.start and item.end <= self.end
elif isinstance(item, datetime.datetime):
return self.start <= item <= self.end
elif isinstance(item, datetime.date):
return self.start.date() <= item <= self.end.date()
# Read-only field accessors
@property
def fiscal_year(self) -> int:
""":returns: The fiscal year"""
return self._fiscal_year
@property
def fiscal_quarter(self) -> int:
""":returns: The fiscal quarter"""
return self._fiscal_quarter
@property
def prev_fiscal_quarter(self) -> "FiscalQuarter":
""":returns: The previous fiscal quarter"""
fiscal_year = self._fiscal_year
fiscal_quarter = self._fiscal_quarter - 1
if fiscal_quarter == 0:
fiscal_year -= 1
fiscal_quarter = 4
return FiscalQuarter(fiscal_year, fiscal_quarter)
@property
def next_fiscal_quarter(self) -> "FiscalQuarter":
""":returns: The next fiscal quarter"""
fiscal_year = self._fiscal_year
fiscal_quarter = self._fiscal_quarter + 1
if fiscal_quarter == 5:
fiscal_year += 1
fiscal_quarter = 1
return FiscalQuarter(fiscal_year, fiscal_quarter)
@property
def start(self) -> "FiscalDateTime":
""":returns: The start of the fiscal quarter"""
# Find the first month of the fiscal quarter
month = START_MONTH
month += (self._fiscal_quarter - 1) * MONTHS_PER_QUARTER
month %= 12
if month == 0:
month = 12
# Find the calendar year of the start of the fiscal quarter
if START_YEAR == "previous":
year = self._fiscal_year - 1
elif START_YEAR == "same":
year = self._fiscal_year
else:
raise ValueError(
"START_YEAR must be either 'previous' or 'same'", START_YEAR
)
if month < START_MONTH:
year += 1
# Find the last day of the month
# If START_DAY is later, choose last day of month instead
max_day = calendar.monthrange(year, month)[1]
day = min(START_DAY, max_day)
return FiscalDateTime(year, month, day, 0, 0, 0)
@property
def end(self) -> "FiscalDateTime":
""":returns: The end of the fiscal quarter"""
# Find the start of the next fiscal quarter
next_start = self.next_fiscal_quarter.start
# Substract 1 second
end = next_start - datetime.timedelta(seconds=1)
return FiscalDateTime(
end.year,
end.month,
end.day,
end.hour,
end.minute,
end.second,
end.microsecond,
end.tzinfo,
)
# Comparisons of FiscalQuarter objects with other
def __lt__(self, other: "FiscalQuarter") -> bool:
return (self._fiscal_year, self._fiscal_quarter) < (
other._fiscal_year,
other._fiscal_quarter,
)
def __le__(self, other: "FiscalQuarter") -> bool:
return (self._fiscal_year, self._fiscal_quarter) <= (
other._fiscal_year,
other._fiscal_quarter,
)
def __eq__(self, other: object) -> bool:
if isinstance(other, FiscalQuarter):
return (self._fiscal_year, self._fiscal_quarter) == (
other._fiscal_year,
other._fiscal_quarter,
)
else:
raise TypeError(
f"can't compare '{type(self).__name__}' to '{type(other).__name__}'"
)
def __ne__(self, other: object) -> bool:
if isinstance(other, FiscalQuarter):
return (self._fiscal_year, self._fiscal_quarter) != (
other._fiscal_year,
other._fiscal_quarter,
)
else:
raise TypeError(
f"can't compare '{type(self).__name__}' to '{type(other).__name__}'"
)
def __gt__(self, other: "FiscalQuarter") -> bool:
return (self._fiscal_year, self._fiscal_quarter) > (
other._fiscal_year,
other._fiscal_quarter,
)
def __ge__(self, other: "FiscalQuarter") -> bool:
return (self._fiscal_year, self._fiscal_quarter) >= (
other._fiscal_year,
other._fiscal_quarter,
)
[docs]class FiscalMonth(_Hashable):
"""A class representing a single fiscal month."""
__slots__ = ["_fiscal_year", "_fiscal_month"]
__hash__ = _Hashable.__hash__
_fiscal_year: int
_fiscal_month: int
def __new__(cls, fiscal_year: int, fiscal_month: int) -> "FiscalMonth":
"""Constructor.
:param fiscal_year: The fiscal year
:param fiscal_month: The fiscal month
:returns: A newly constructed FiscalMonth object
:raises ValueError: If fiscal_year or fiscal_month is out of range
"""
fiscal_year = _check_year(fiscal_year)
fiscal_month = _check_month(fiscal_month)
self = super(FiscalMonth, cls).__new__(cls)
self._fiscal_year = fiscal_year
self._fiscal_month = fiscal_month
return self
[docs] @classmethod
def current(cls) -> "FiscalMonth":
"""Alternative constructor. Returns the current FiscalMonth.
:returns: A newly constructed FiscalMonth object
"""
today = FiscalDate.today()
return cls(today.fiscal_year, today.fiscal_month)
def __repr__(self) -> str:
"""Convert to formal string, for repr().
>>> fm = FiscalMonth(2017, 1)
>>> repr(fm)
'FiscalMonth(2017, 1)'
"""
return f"{self.__class__.__name__}({self._fiscal_year}, {self._fiscal_month})"
def __str__(self) -> str:
"""Convert to informal string, for str().
>>> fm = FiscalMonth(2017, 1)
>>> str(fm)
'FY2017 FM1'
"""
return f"FY{self._fiscal_year} FM{self._fiscal_month}"
# TODO: Implement __format__ so that you can print
# fiscal year as 17 or 2017 (%y or %Y)
def __contains__(
self, item: Union["FiscalMonth", "FiscalDay", datetime.datetime, datetime.date]
) -> bool:
"""Returns True if item in self, else False.
:param item: The item to check
"""
if isinstance(item, FiscalMonth):
return self == item
elif isinstance(item, FiscalDay):
return self.start <= item.start <= item.end <= self.end
elif isinstance(item, datetime.datetime):
return self.start <= item <= self.end
elif isinstance(item, datetime.date):
return self.start.date() <= item <= self.end.date()
# Read-only field accessors
@property
def fiscal_year(self) -> int:
""":returns: The fiscal year"""
return self._fiscal_year
@property
def fiscal_month(self) -> int:
""":returns: The fiscal month"""
return self._fiscal_month
@property
def start(self) -> "FiscalDateTime":
""":returns: Start of the fiscal month"""
calendar_month = (START_MONTH + self._fiscal_month - 1) % 12
if calendar_month == 0:
calendar_month = 12
month_is_on_or_after_start_month = calendar_month >= START_MONTH
if START_YEAR == "previous":
if month_is_on_or_after_start_month:
calendar_year = self._fiscal_year - 1
else:
calendar_year = self._fiscal_year
elif START_YEAR == "same":
if month_is_on_or_after_start_month:
calendar_year = self._fiscal_year
else:
calendar_year = self._fiscal_year + 1
return FiscalDateTime(calendar_year, calendar_month, START_DAY)
@property
def end(self) -> "FiscalDateTime":
""":returns: End of the fiscal month"""
# Find the start of the next fiscal quarter
next_start = self.next_fiscal_month.start
# Substract 1 second
end = next_start - datetime.timedelta(seconds=1)
return FiscalDateTime(
end.year,
end.month,
end.day,
end.hour,
end.minute,
end.second,
end.microsecond,
end.tzinfo,
)
@property
def prev_fiscal_month(self) -> "FiscalMonth":
""":returns: The previous fiscal month"""
fiscal_year = self._fiscal_year
fiscal_month = self._fiscal_month - 1
if fiscal_month == 0:
fiscal_year -= 1
fiscal_month = 12
return FiscalMonth(fiscal_year, fiscal_month)
@property
def next_fiscal_month(self) -> "FiscalMonth":
""":returns: The next fiscal month"""
fiscal_year = self._fiscal_year
fiscal_month = self._fiscal_month + 1
if fiscal_month == 13:
fiscal_year += 1
fiscal_month = 1
return FiscalMonth(fiscal_year, fiscal_month)
# Comparisons of FiscalMonth objects with other
def __lt__(self, other: "FiscalMonth") -> bool:
return (self._fiscal_year, self._fiscal_month) < (
other._fiscal_year,
other._fiscal_month,
)
def __le__(self, other: "FiscalMonth") -> bool:
return (self._fiscal_year, self._fiscal_month) <= (
other._fiscal_year,
other._fiscal_month,
)
def __eq__(self, other: object) -> bool:
if isinstance(other, FiscalMonth):
return (self._fiscal_year, self._fiscal_month) == (
other._fiscal_year,
other._fiscal_month,
)
else:
raise TypeError(
f"can't compare '{type(self).__name__}' to '{type(other).__name__}'"
)
def __ne__(self, other: object) -> bool:
if isinstance(other, FiscalMonth):
return (self._fiscal_year, self._fiscal_month) != (
other._fiscal_year,
other._fiscal_month,
)
else:
raise TypeError(
f"can't compare '{type(self).__name__}' to '{type(other).__name__}'"
)
def __gt__(self, other: "FiscalMonth") -> bool:
return (self._fiscal_year, self._fiscal_month) > (
other._fiscal_year,
other._fiscal_month,
)
def __ge__(self, other: "FiscalMonth") -> bool:
return (self._fiscal_year, self._fiscal_month) >= (
other._fiscal_year,
other._fiscal_month,
)
[docs]class FiscalDay(_Hashable):
"""A class representing a single fiscal day."""
__slots__ = ["_fiscal_year", "_fiscal_day"]
__hash__ = _Hashable.__hash__
_fiscal_year: int
_fiscal_day: int
def __new__(cls, fiscal_year: int, fiscal_day: int) -> "FiscalDay":
"""Constructor.
:param fiscal_year: The fiscal year
:param fiscal_day: The fiscal day
:returns: A newly constructed FiscalDay object
:raises ValueError: If fiscal_year or fiscal_day is out of range
"""
fiscal_year = _check_year(fiscal_year)
fiscal_day = _check_fiscal_day(fiscal_year, fiscal_day)
self = super(FiscalDay, cls).__new__(cls)
self._fiscal_year = fiscal_year
self._fiscal_day = fiscal_day
return self
[docs] @classmethod
def current(cls) -> "FiscalDay":
"""Alternative constructor. Returns the current FiscalDay.
:returns: A newly constructed FiscalDay object
"""
today = FiscalDate.today()
return cls(today.fiscal_year, today.fiscal_day)
def __repr__(self) -> str:
"""Convert to formal string, for repr().
>>> fd = FiscalDay(2017, 1)
>>> repr(fd)
'FiscalDay(2017, 1)'
"""
return f"{self.__class__.__name__}({self._fiscal_year}, {self._fiscal_day})"
def __str__(self) -> str:
"""Convert to informal string, for str().
>>> fd = FiscalDay(2017, 1)
>>> str(fd)
'FY2017 FD1'
"""
return f"FY{self._fiscal_year} FD{self._fiscal_day}"
# TODO: Implement __format__ so that you can print
# fiscal year as 17 or 2017 (%y or %Y)
def __contains__(
self, item: Union["FiscalDay", datetime.datetime, datetime.date]
) -> bool:
"""Returns True if item in self, else False.
:param item: The item to check
"""
if isinstance(item, FiscalDay):
return self == item
elif isinstance(item, datetime.datetime):
return self.start <= item <= self.end
elif isinstance(item, datetime.date):
return self.start.date() <= item <= self.end.date()
# Read-only field accessors
@property
def fiscal_year(self) -> int:
""":returns: The fiscal year"""
return self._fiscal_year
@property
def fiscal_quarter(self) -> int:
""":returns: The fiscal quarter"""
return self.start.fiscal_quarter
@property
def fiscal_month(self) -> int:
""":returns: The fiscal month"""
return self.start.fiscal_month
@property
def fiscal_day(self) -> int:
""":returns: The fiscal day"""
return self._fiscal_day
@property
def start(self) -> "FiscalDateTime":
""":returns: Start of the fiscal day"""
fiscal_year = FiscalYear(self._fiscal_year)
days_elapsed = datetime.timedelta(days=self._fiscal_day - 1)
start = fiscal_year.start + days_elapsed
return FiscalDateTime(start.year, start.month, start.day, 0, 0, 0)
@property
def end(self) -> "FiscalDateTime":
""":returns: End of the fiscal day"""
# Find the start of the next fiscal quarter
next_start = self.next_fiscal_day.start
# Substract 1 second
end = next_start - datetime.timedelta(seconds=1)
return FiscalDateTime(
end.year,
end.month,
end.day,
end.hour,
end.minute,
end.second,
end.microsecond,
end.tzinfo,
)
@property
def prev_fiscal_day(self) -> "FiscalDay":
""":returns: The previous fiscal day"""
fiscal_year = self._fiscal_year
fiscal_day = self._fiscal_day - 1
if fiscal_day == 0:
fiscal_year -= 1
try:
fiscal_day = _check_fiscal_day(fiscal_year, 366)
except ValueError:
fiscal_day = _check_fiscal_day(fiscal_year, 365)
return FiscalDay(fiscal_year, fiscal_day)
@property
def next_fiscal_day(self) -> "FiscalDay":
""":returns: The next fiscal day"""
fiscal_year = self._fiscal_year
try:
fiscal_day = _check_fiscal_day(fiscal_year, self._fiscal_day + 1)
except ValueError:
fiscal_year += 1
fiscal_day = 1
return FiscalDay(fiscal_year, fiscal_day)
# Comparisons of FiscalDay objects with other
def __lt__(self, other: "FiscalDay") -> bool:
return (self._fiscal_year, self._fiscal_day) < (
other._fiscal_year,
other._fiscal_day,
)
def __le__(self, other: "FiscalDay") -> bool:
return (self._fiscal_year, self._fiscal_day) <= (
other._fiscal_year,
other._fiscal_day,
)
def __eq__(self, other: object) -> bool:
if isinstance(other, FiscalDay):
return (self._fiscal_year, self._fiscal_day) == (
other._fiscal_year,
other._fiscal_day,
)
else:
raise TypeError(
f"can't compare '{type(self).__name__}' to '{type(other).__name__}'"
)
def __ne__(self, other: object) -> bool:
if isinstance(other, FiscalDay):
return (self._fiscal_year, self._fiscal_day) != (
other._fiscal_year,
other._fiscal_day,
)
else:
raise TypeError(
f"can't compare '{type(self).__name__}' to '{type(other).__name__}'"
)
def __gt__(self, other: "FiscalDay") -> bool:
return (self._fiscal_year, self._fiscal_day) > (
other._fiscal_year,
other._fiscal_day,
)
def __ge__(self, other: "FiscalDay") -> bool:
return (self._fiscal_year, self._fiscal_day) >= (
other._fiscal_year,
other._fiscal_day,
)
class _FiscalMixin:
"""Mixin for FiscalDate and FiscalDateTime that
provides the following common attributes in addition to
those provided by datetime.date and datetime.datetime:
"""
@property
def fiscal_year(self) -> int:
""":returns: The fiscal year"""
fiscal_self = cast(Union["FiscalDate", "FiscalDateTime"], self)
# The fiscal year can be at most 1 year away from the calendar year
if fiscal_self in FiscalYear(fiscal_self.year):
return fiscal_self.year
elif fiscal_self in FiscalYear(fiscal_self.year + 1):
return fiscal_self.year + 1
else:
return fiscal_self.year - 1
@property
def fiscal_quarter(self) -> int:
""":returns: The fiscal quarter"""
fiscal_self = cast(Union["FiscalDate", "FiscalDateTime"], self)
for quarter in range(1, 5):
q = FiscalQuarter(fiscal_self.fiscal_year, quarter)
if fiscal_self in q:
break
return quarter
@property
def fiscal_month(self) -> int:
""":returns: The fiscal month"""
fiscal_self = cast(Union["FiscalDate", "FiscalDateTime"], self)
for month in range(1, 13):
m = FiscalMonth(fiscal_self.fiscal_year, month)
if fiscal_self in m:
break
return month
@property
def fiscal_day(self) -> int:
""":returns: The fiscal day"""
fiscal_self = cast(Union["FiscalDate", "FiscalDateTime"], self)
fiscal_year = FiscalYear(fiscal_self.fiscal_year)
year_start = fiscal_year.start
if isinstance(fiscal_self, FiscalDate):
delta = cast(datetime.date, fiscal_self) - year_start.date()
else:
delta = fiscal_self - year_start
return delta.days + 1
@property
def prev_fiscal_year(self) -> FiscalYear:
""":returns: The previous fiscal year"""
return FiscalYear(self.fiscal_year - 1)
@property
def next_fiscal_year(self) -> FiscalYear:
""":returns: The next fiscal year"""
return FiscalYear(self.fiscal_year + 1)
@property
def prev_fiscal_quarter(self) -> FiscalQuarter:
""":returns: The previous fiscal quarter"""
fiscal_quarter = FiscalQuarter(self.fiscal_year, self.fiscal_quarter)
return fiscal_quarter.prev_fiscal_quarter
@property
def next_fiscal_quarter(self) -> FiscalQuarter:
""":returns: The next fiscal quarter"""
fiscal_quarter = FiscalQuarter(self.fiscal_year, self.fiscal_quarter)
return fiscal_quarter.next_fiscal_quarter
@property
def prev_fiscal_month(self) -> FiscalMonth:
""":returns: The previous fiscal month"""
fiscal_month = FiscalMonth(self.fiscal_year, self.fiscal_month)
return fiscal_month.prev_fiscal_month
@property
def next_fiscal_month(self) -> FiscalMonth:
""":returns: The next fiscal month"""
fiscal_month = FiscalMonth(self.fiscal_year, self.fiscal_month)
return fiscal_month.next_fiscal_month
@property
def prev_fiscal_day(self) -> FiscalDay:
""":returns: The previous fiscal day"""
fiscal_day = FiscalDay(self.fiscal_year, self.fiscal_day)
return fiscal_day.prev_fiscal_day
@property
def next_fiscal_day(self) -> FiscalDay:
""":returns: The next fiscal day"""
fiscal_day = FiscalDay(self.fiscal_year, self.fiscal_day)
return fiscal_day.next_fiscal_day
[docs]class FiscalDate(datetime.date, _FiscalMixin):
"""A wrapper around the builtin datetime.date class
that provides the following attributes."""
pass
[docs]class FiscalDateTime(datetime.datetime, _FiscalMixin):
"""A wrapper around the builtin datetime.datetime class
that provides the following attributes."""
pass