Coverage for jstark / features / first_and_last_date_of_period.py: 100%
55 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-30 09:29 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-30 09:29 +0000
1"""FirstAndLastDateOfPeriod class
3Helper class for figuring out dates relative to a given date
4"""
6from datetime import date, timedelta
8import pendulum
11class FirstAndLastDateOfPeriod:
12 """Encapsulate all the logic to determine first and last date
13 of a period that includes the supplied date
14 """
16 def __init__(
17 self, date_in_period: date, first_day_of_week: str | None = None
18 ) -> None:
19 self._date_in_period = date_in_period
20 self._weekdays = [
21 "Monday",
22 "Tuesday",
23 "Wednesday",
24 "Thursday",
25 "Friday",
26 "Saturday",
27 "Sunday",
28 ]
29 if first_day_of_week is None:
30 # use what python determines to be the first day of the week
31 first_day_of_week = (
32 date.today() - timedelta(date.today().weekday())
33 ).strftime("%A")
34 if first_day_of_week not in self._weekdays:
35 raise ValueError(f"first_day_of_week must be one of {self._weekdays}")
36 self._first_day_of_week = first_day_of_week
38 @property
39 def first_date_in_week(self) -> date:
40 current_weekday_index = self._weekdays.index(
41 self._date_in_period.strftime("%A")
42 )
43 first_day_index = self._weekdays.index(self._first_day_of_week)
44 # Number of days to subtract to get to the first day of this week (may be 0)
45 days_to_subtract = (current_weekday_index - first_day_index) % 7
46 return self._date_in_period - timedelta(days=days_to_subtract)
48 @property
49 def last_date_in_week(self) -> date:
50 return self.first_date_in_week + timedelta(days=6)
52 @property
53 def first_date_in_month(self) -> date:
54 return date(self._date_in_period.year, self._date_in_period.month, 1)
56 @property
57 def last_date_in_month(self) -> date:
58 first_of_month = pendulum.date(
59 self._date_in_period.year, self._date_in_period.month, 1
60 )
61 return first_of_month.add(months=1).subtract(days=1)
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)