Source code for fiscalyear

"""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