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

1"""FirstAndLastDateOfPeriod class 

2 

3Helper class for figuring out dates relative to a given date 

4""" 

5 

6from datetime import date, timedelta 

7from dateutil.relativedelta import relativedelta 

8 

9 

10class FirstAndLastDateOfPeriod: 

11 """Encapsulate all the logic to determine first and last date 

12 of a period that includes the supplied date 

13 """ 

14 

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 

36 

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) 

46 

47 @property 

48 def last_date_in_week(self) -> date: 

49 return self.first_date_in_week + timedelta(days=6) 

50 

51 @property 

52 def first_date_in_month(self) -> date: 

53 return date(self._date_in_period.year, self._date_in_period.month, 1) 

54 

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 ) 

62 

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) 

74 

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) 

86 

87 @property 

88 def first_date_in_year(self) -> date: 

89 return date(self._date_in_period.year, 1, 1) 

90 

91 @property 

92 def last_date_in_year(self) -> date: 

93 return date(self._date_in_period.year, 12, 31)