11import datetime
2+ from calendar import monthrange
23from enum import Enum
34
45from plain .utils import timezone
@@ -35,72 +36,90 @@ class DatetimeRangeAliases(Enum):
3536 # TODO doesn't include anything less than a day...
3637 # ex. SINCE_1_HOUR_AGO = "Since 1 Hour Ago"
3738
39+ def __str__ (self ):
40+ return self .value
41+
42+ @classmethod
43+ def from_value (cls , value ):
44+ for member in cls :
45+ if member .value == value :
46+ return member
47+ raise ValueError (f"{ value } is not a valid value for { cls .__name__ } " )
48+
3849 @classmethod
39- def to_range (cls , value : str ) -> ( datetime .datetime , datetime .datetime ) :
50+ def to_range (cls , value : str ) -> tuple [ datetime .datetime , datetime .datetime ] :
4051 now = timezone .localtime ()
41- start_of_week = now - datetime .timedelta (days = now .weekday ())
42- start_of_month = now .replace (day = 1 )
43- start_of_quarter = now .replace (month = ((now .month - 1 ) // 3 ) * 3 + 1 , day = 1 )
44- start_of_year = now .replace (month = 1 , day = 1 )
52+ start_of_today = now .replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
53+ start_of_week = start_of_today - datetime .timedelta (
54+ days = start_of_today .weekday ()
55+ )
56+ start_of_month = start_of_today .replace (day = 1 )
57+ start_of_quarter = start_of_today .replace (
58+ month = ((start_of_today .month - 1 ) // 3 ) * 3 + 1 , day = 1
59+ )
60+ start_of_year = start_of_today .replace (month = 1 , day = 1 )
61+
62+ def end_of_day (dt ):
63+ return dt .replace (hour = 23 , minute = 59 , second = 59 , microsecond = 999999 )
64+
65+ def end_of_month (dt ):
66+ last_day = monthrange (dt .year , dt .month )[1 ]
67+ return end_of_day (dt .replace (day = last_day ))
68+
69+ def end_of_quarter (dt ):
70+ end_month = ((dt .month - 1 ) // 3 + 1 ) * 3
71+ return end_of_month (dt .replace (month = end_month ))
72+
73+ def end_of_year (dt ):
74+ return end_of_month (dt .replace (month = 12 ))
4575
4676 if value == cls .TODAY :
47- return DatetimeRange (now , now )
77+ return DatetimeRange (start_of_today , end_of_day ( now ) )
4878 if value == cls .THIS_WEEK :
4979 return DatetimeRange (
50- start_of_week , start_of_week + datetime .timedelta (days = 6 )
80+ start_of_week , end_of_day ( start_of_week + datetime .timedelta (days = 6 ) )
5181 )
5282 if value == cls .THIS_WEEK_TO_DATE :
5383 return DatetimeRange (start_of_week , now )
5484 if value == cls .THIS_MONTH :
55- return DatetimeRange (
56- start_of_month , start_of_month + datetime .timedelta (days = 31 )
57- )
85+ return DatetimeRange (start_of_month , end_of_month (start_of_month ))
5886 if value == cls .THIS_MONTH_TO_DATE :
5987 return DatetimeRange (start_of_month , now )
6088 if value == cls .THIS_QUARTER :
61- return DatetimeRange (
62- start_of_quarter , start_of_quarter + datetime .timedelta (days = 90 )
63- )
89+ return DatetimeRange (start_of_quarter , end_of_quarter (start_of_quarter ))
6490 if value == cls .THIS_QUARTER_TO_DATE :
6591 return DatetimeRange (start_of_quarter , now )
6692 if value == cls .THIS_YEAR :
67- return DatetimeRange (
68- start_of_year ,
69- start_of_year .replace (year = start_of_year .year + 1 )
70- - datetime .timedelta (days = 1 ),
71- )
93+ return DatetimeRange (start_of_year , end_of_year (start_of_year ))
7294 if value == cls .THIS_YEAR_TO_DATE :
7395 return DatetimeRange (start_of_year , now )
7496 if value == cls .LAST_WEEK :
97+ last_week_start = start_of_week - datetime .timedelta (days = 7 )
7598 return DatetimeRange (
76- start_of_week - datetime . timedelta ( days = 7 ) ,
77- start_of_week - datetime .timedelta (days = 1 ),
99+ last_week_start ,
100+ end_of_day ( last_week_start + datetime .timedelta (days = 6 ) ),
78101 )
79102 if value == cls .LAST_WEEK_TO_DATE :
80103 return DatetimeRange (start_of_week - datetime .timedelta (days = 7 ), now )
81104 if value == cls .LAST_MONTH :
82105 last_month = (start_of_month - datetime .timedelta (days = 1 )).replace (day = 1 )
83- return DatetimeRange (
84- last_month , last_month .replace (day = 1 ) + datetime .timedelta (days = 31 )
85- )
106+ return DatetimeRange (last_month , end_of_month (last_month ))
86107 if value == cls .LAST_MONTH_TO_DATE :
87108 last_month = (start_of_month - datetime .timedelta (days = 1 )).replace (day = 1 )
88109 return DatetimeRange (last_month , now )
89110 if value == cls .LAST_QUARTER :
90111 last_quarter = (start_of_quarter - datetime .timedelta (days = 1 )).replace (
91112 day = 1
92113 )
93- return DatetimeRange (
94- last_quarter , last_quarter + datetime .timedelta (days = 90 )
95- )
114+ return DatetimeRange (last_quarter , end_of_quarter (last_quarter ))
96115 if value == cls .LAST_QUARTER_TO_DATE :
97116 last_quarter = (start_of_quarter - datetime .timedelta (days = 1 )).replace (
98117 day = 1
99118 )
100119 return DatetimeRange (last_quarter , now )
101120 if value == cls .LAST_YEAR :
102121 last_year = start_of_year .replace (year = start_of_year .year - 1 )
103- return DatetimeRange (last_year , start_of_year - datetime . timedelta ( days = 1 ))
122+ return DatetimeRange (last_year , end_of_year ( last_year ))
104123 if value == cls .LAST_YEAR_TO_DATE :
105124 last_year = start_of_year .replace (year = start_of_year .year - 1 )
106125 return DatetimeRange (last_year , now )
@@ -113,28 +132,24 @@ def to_range(cls, value: str) -> (datetime.datetime, datetime.datetime):
113132 if value == cls .SINCE_365_DAYS_AGO :
114133 return DatetimeRange (now - datetime .timedelta (days = 365 ), now )
115134 if value == cls .NEXT_WEEK :
135+ next_week_start = start_of_week + datetime .timedelta (days = 7 )
116136 return DatetimeRange (
117- start_of_week + datetime . timedelta ( days = 7 ) ,
118- start_of_week + datetime .timedelta (days = 13 ),
137+ next_week_start ,
138+ end_of_day ( next_week_start + datetime .timedelta (days = 6 ) ),
119139 )
120140 if value == cls .NEXT_4_WEEKS :
121- return DatetimeRange (now , now + datetime .timedelta (days = 28 ))
141+ return DatetimeRange (now , end_of_day ( now + datetime .timedelta (days = 28 ) ))
122142 if value == cls .NEXT_MONTH :
123143 next_month = (start_of_month + datetime .timedelta (days = 31 )).replace (day = 1 )
124- return DatetimeRange (next_month , next_month + datetime . timedelta ( days = 31 ))
144+ return DatetimeRange (next_month , end_of_month ( next_month ))
125145 if value == cls .NEXT_QUARTER :
126146 next_quarter = (start_of_quarter + datetime .timedelta (days = 90 )).replace (
127147 day = 1
128148 )
129- return DatetimeRange (
130- next_quarter , next_quarter + datetime .timedelta (days = 90 )
131- )
149+ return DatetimeRange (next_quarter , end_of_quarter (next_quarter ))
132150 if value == cls .NEXT_YEAR :
133151 next_year = start_of_year .replace (year = start_of_year .year + 1 )
134- return DatetimeRange (
135- next_year ,
136- next_year .replace (year = next_year .year + 1 ) - datetime .timedelta (days = 1 ),
137- )
152+ return DatetimeRange (next_year , end_of_year (next_year ))
138153 raise ValueError (f"Invalid range: { value } " )
139154
140155
@@ -149,12 +164,16 @@ def __init__(self, start, end):
149164 if isinstance (self .end , str ) and self .end :
150165 self .end = datetime .datetime .fromisoformat (self .end )
151166
152- if isinstance (self .start , datetime .date ):
167+ if isinstance (self .start , datetime .date ) and not isinstance (
168+ self .start , datetime .datetime
169+ ):
153170 self .start = timezone .localtime ().replace (
154171 year = self .start .year , month = self .start .month , day = self .start .day
155172 )
156173
157- if isinstance (self .end , datetime .date ):
174+ if isinstance (self .end , datetime .date ) and not isinstance (
175+ self .start , datetime .datetime
176+ ):
158177 self .end = timezone .localtime ().replace (
159178 year = self .end .year , month = self .end .month , day = self .end .day
160179 )
0 commit comments