Coverage for jstark / features / first_and_last_date_of_period.py: 100%
54 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-23 22:34 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-23 22:34 +0000
1"""FirstAndLastDateOfPeriod class
3Helper class for figuring out dates relative to a given date
4"""
6from datetime import date, timedelta
7from dateutil.relativedelta import relativedelta
10class FirstAndLastDateOfPeriod:
11 """Encapsulate all the logic to determine first and last date
12 of a period that includes the supplied date
13 """
15 def __init__(
16 self, date_in_period: date, first_day_of_week: str | None = None
17 ) -> None:
18 self._date_in_period = date_in_period
19 self._weekdays = [
20 "Monday",
21 "Tuesday",
22 "Wednesday",
23 "Thursday",
24 "Friday",
25 "Saturday",
26 "Sunday",
27 ]
28 if first_day_of_week is None:
29 # use what python determines to be the first day of the week
30 first_day_of_week = (
31 date.today() - timedelta(date.today().weekday())
32 ).strftime("%A")
33 if first_day_of_week not in self._weekdays:
34 raise ValueError(f"first_day_of_week must be one of {self._weekdays}")
35 self._first_day_of_week = first_day_of_week
37 @property
38 def first_date_in_week(self) -> date:
39 current_weekday_index = self._weekdays.index(
40 self._date_in_period.strftime("%A")
41 )
42 first_day_index = self._weekdays.index(self._first_day_of_week)
43 # Number of days to subtract to get to the first day of this week (may be 0)
44 days_to_subtract = (current_weekday_index - first_day_index) % 7
45 return self._date_in_period - timedelta(days=days_to_subtract)
47 @property
48 def last_date_in_week(self) -> date:
49 return self.first_date_in_week + timedelta(days=6)
51 @property
52 def first_date_in_month(self) -> date:
53 return date(self._date_in_period.year, self._date_in_period.month, 1)
55 @property
56 def last_date_in_month(self) -> date:
57 return (
58 self._date_in_period
59 + relativedelta(months=1, day=1)
60 - relativedelta(days=1)
61 )
63 @property
64 def first_date_in_quarter(self) -> date:
65 match self._date_in_period.month:
66 case 1 | 2 | 3:
67 return date(self._date_in_period.year, 1, 1)
68 case 4 | 5 | 6:
69 return date(self._date_in_period.year, 4, 1)
70 case 7 | 8 | 9:
71 return date(self._date_in_period.year, 7, 1)
72 case _: # all other months:
73 return date(self._date_in_period.year, 10, 1)
75 @property
76 def last_date_in_quarter(self) -> date:
77 match self._date_in_period.month:
78 case 1 | 2 | 3:
79 return date(self._date_in_period.year, 3, 31)
80 case 4 | 5 | 6:
81 return date(self._date_in_period.year, 6, 30)
82 case 7 | 8 | 9:
83 return date(self._date_in_period.year, 9, 30)
84 case _: # all other months:
85 return date(self._date_in_period.year, 12, 31)
87 @property
88 def first_date_in_year(self) -> date:
89 return date(self._date_in_period.year, 1, 1)
91 @property
92 def last_date_in_year(self) -> date:
93 return date(self._date_in_period.year, 12, 31)