Coverage for jutil/dates.py: 71%
154 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-07 16:40 -0500
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-07 16:40 -0500
1from datetime import datetime, timedelta
2from typing import Tuple, Any, Optional, List
3import pytz
4from calendar import monthrange
5from django.utils.text import format_lazy
6from django.utils.translation import gettext_lazy as _
9TIME_RANGE_CHOICES = [
10 ("yesterday", _("yesterday")),
11 ("today", _("today")),
12 ("tomorrow", _("tomorrow")),
13 ("last_week", _("last week")),
14 ("last_month", _("last month")),
15 ("last_year", _("last year")),
16 ("this_week", _("this week")),
17 ("this_month", _("this month")),
18 ("this_year", _("this year")),
19 ("next_week", _("next week")),
20 ("next_month", _("next month")),
21 ("next_year", _("next year")),
22]
23# plus +- date ranges from current datetime:
24# (e.g. --yesterday is full day yesterday but --prev-1d is 24h less from current time)
25for d in [360, 180, 90, 60, 45, 30, 15, 7, 2, 1]:
26 TIME_RANGE_CHOICES.extend(
27 [
28 ("prev_{}d".format(d), format_lazy("-{} {}", d, _("number.of.days"))),
29 ("plus_minus_{}d".format(d), format_lazy("+-{} {}", d, _("number.of.days"))),
30 ("next_{}d".format(d), format_lazy("+{} {}", d, _("number.of.days"))),
31 ]
32 )
34TIME_RANGE_NAMES = list(zip(*TIME_RANGE_CHOICES))[0]
36TIME_STEP_DAILY = "daily"
37TIME_STEP_WEEKLY = "weekly"
38TIME_STEP_MONTHLY = "monthly"
40TIME_STEP_TYPES = [
41 TIME_STEP_DAILY,
42 TIME_STEP_WEEKLY,
43 TIME_STEP_MONTHLY,
44]
46TIME_STEP_CHOICES = [
47 (TIME_STEP_DAILY, _("daily")),
48 (TIME_STEP_WEEKLY, _("weekly")),
49 (TIME_STEP_MONTHLY, _("monthly")),
50]
52TIME_STEP_NAMES = list(zip(*TIME_STEP_CHOICES))[0]
55def localize_time_range(begin: datetime, end: datetime, tz: Any = None) -> Tuple[datetime, datetime]:
56 """
57 Localizes time range. Uses pytz.utc if None provided.
58 :param begin: Begin datetime
59 :param end: End datetime
60 :param tz: pytz timezone or None (default UTC)
61 :return: begin, end
62 """
63 if tz is None: 63 ↛ 65line 63 didn't jump to line 65, because the condition on line 63 was never false
64 tz = pytz.utc
65 return tz.localize(begin), tz.localize(end)
68def get_last_day_of_month(today: Optional[datetime] = None) -> int:
69 """
70 Returns day number of the last day of the month
71 :param today: Default UTC now
72 :return: int
73 """
74 if today is None:
75 today = datetime.utcnow()
76 return monthrange(today.year, today.month)[1]
79def end_of_month(today: Optional[datetime] = None, n: int = 0, tz: Any = None) -> datetime:
80 """
81 Returns end-of-month (last microsecond) of given datetime (or current datetime UTC if no parameter is passed).
82 :param today: Some date in the month (defaults current datetime)
83 :param n: +- number of months to offset from current month. Default 0.
84 :param tz: Timezone (defaults pytz UTC)
85 :return: datetime
86 """
87 if today is None: 87 ↛ 88line 87 didn't jump to line 88, because the condition on line 87 was never true
88 today = datetime.utcnow()
89 last_day = monthrange(today.year, today.month)[1]
90 end = today.replace(day=last_day, hour=0, minute=0, second=0, microsecond=0) + timedelta(hours=24)
91 while n > 0:
92 last_day = monthrange(end.year, end.month)[1]
93 end = end.replace(day=last_day, hour=0, minute=0, second=0, microsecond=0) + timedelta(hours=24)
94 n -= 1
95 while n < 0:
96 end -= timedelta(days=1)
97 end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
98 n += 1
99 end_incl = end - timedelta(microseconds=1)
100 if tz is None: 100 ↛ 101line 100 didn't jump to line 101, because the condition on line 100 was never true
101 tz = pytz.utc
102 return tz.localize(end_incl)
105def this_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
106 """
107 Returns this week begin (inclusive) and end (exclusive).
108 Week is assumed to start from Monday (ISO).
109 :param today: Some date (defaults current datetime)
110 :param tz: Timezone (defaults pytz UTC)
111 :return: begin (inclusive), end (exclusive)
112 """
113 if today is None: 113 ↛ 114line 113 didn't jump to line 114, because the condition on line 113 was never true
114 today = datetime.utcnow()
115 begin = today - timedelta(days=today.weekday())
116 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
117 return localize_time_range(begin, begin + timedelta(days=7), tz)
120def this_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
121 """
122 Returns current month begin (inclusive) and end (exclusive).
123 :param today: Some date in the month (defaults current datetime)
124 :param tz: Timezone (defaults pytz UTC)
125 :return: begin (inclusive), end (exclusive)
126 """
127 if today is None: 127 ↛ 128line 127 didn't jump to line 128, because the condition on line 127 was never true
128 today = datetime.utcnow()
129 begin = datetime(day=1, month=today.month, year=today.year)
130 end = begin + timedelta(days=32)
131 end = datetime(day=1, month=end.month, year=end.year)
132 return localize_time_range(begin, end, tz)
135def this_year(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
136 """
137 Returns this year begin (inclusive) and end (exclusive).
138 :param today: Some date (defaults current datetime)
139 :param tz: Timezone (defaults pytz UTC)
140 :return: begin (inclusive), end (exclusive)
141 """
142 if today is None: 142 ↛ 143line 142 didn't jump to line 143, because the condition on line 142 was never true
143 today = datetime.utcnow()
144 begin = datetime(day=1, month=1, year=today.year)
145 end = datetime(day=1, month=1, year=begin.year + 1)
146 return localize_time_range(begin, end, tz)
149def next_year(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
150 if today is None:
151 today = datetime.utcnow()
152 begin = datetime(day=1, month=1, year=today.year + 1)
153 end = datetime(day=1, month=1, year=begin.year + 2)
154 return localize_time_range(begin, end, tz)
157def next_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
158 """
159 Returns next week begin (inclusive) and end (exclusive).
160 :param today: Some date (defaults current datetime)
161 :param tz: Timezone (defaults pytz UTC)
162 :return: begin (inclusive), end (exclusive)
163 """
164 if today is None: 164 ↛ 165line 164 didn't jump to line 165, because the condition on line 164 was never true
165 today = datetime.utcnow()
166 begin = today + timedelta(days=7 - today.weekday())
167 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
168 return localize_time_range(begin, begin + timedelta(days=7), tz)
171def next_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
172 """
173 Returns next month begin (inclusive) and end (exclusive).
174 :param today: Some date in the month (defaults current datetime)
175 :param tz: Timezone (defaults pytz UTC)
176 :return: begin (inclusive), end (exclusive)
177 """
178 if today is None:
179 today = datetime.utcnow()
180 begin = datetime(day=1, month=today.month, year=today.year)
181 next_mo = begin + timedelta(days=32)
182 begin = datetime(day=1, month=next_mo.month, year=next_mo.year)
183 following_mo = begin + timedelta(days=32)
184 end = datetime(day=1, month=following_mo.month, year=following_mo.year)
185 return localize_time_range(begin, end, tz)
188def last_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
189 """
190 Returns last week begin (inclusive) and end (exclusive).
191 :param today: Some date (defaults current datetime)
192 :param tz: Timezone (defaults pytz UTC)
193 :return: begin (inclusive), end (exclusive)
194 """
195 if today is None: 195 ↛ 196line 195 didn't jump to line 196, because the condition on line 195 was never true
196 today = datetime.utcnow()
197 begin = today - timedelta(weeks=1, days=today.weekday())
198 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
199 return localize_time_range(begin, begin + timedelta(days=7), tz)
202def last_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
203 """
204 Returns last month begin (inclusive) and end (exclusive).
205 :param today: Some date (defaults current datetime)
206 :param tz: Timezone (defaults pytz UTC)
207 :return: begin (inclusive), end (exclusive)
208 """
209 if today is None: 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true
210 today = datetime.utcnow()
211 end = datetime(day=1, month=today.month, year=today.year)
212 end_incl = end - timedelta(seconds=1)
213 begin = datetime(day=1, month=end_incl.month, year=end_incl.year)
214 return localize_time_range(begin, end, tz)
217def last_year(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
218 """
219 Returns last year begin (inclusive) and end (exclusive).
220 :param today: Some date (defaults current datetime)
221 :param tz: Timezone (defaults pytz UTC)
222 :return: begin (inclusive), end (exclusive)
223 """
224 if today is None: 224 ↛ 225line 224 didn't jump to line 225, because the condition on line 224 was never true
225 today = datetime.utcnow()
226 end = datetime(day=1, month=1, year=today.year)
227 end_incl = end - timedelta(seconds=1)
228 begin = datetime(day=1, month=1, year=end_incl.year)
229 return localize_time_range(begin, end, tz)
232def yesterday(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
233 """
234 Returns yesterday begin (inclusive) and end (exclusive).
235 :param today: Some date (defaults current datetime)
236 :param tz: Timezone (defaults pytz UTC)
237 :return: begin (inclusive), end (exclusive)
238 """
239 if today is None: 239 ↛ 240line 239 didn't jump to line 240, because the condition on line 239 was never true
240 today = datetime.utcnow()
241 end = datetime(day=today.day, month=today.month, year=today.year)
242 end_incl = end - timedelta(seconds=1)
243 begin = datetime(day=end_incl.day, month=end_incl.month, year=end_incl.year)
244 return localize_time_range(begin, end, tz)
247def add_month(t: datetime, n: int = 1) -> datetime:
248 """
249 Adds +- n months to datetime.
250 Clamps days to number of days in given month.
251 :param t: datetime
252 :param n: +- number of months to offset from current month. Default 1.
253 :return: datetime
254 """
255 t2 = t
256 for count in range(abs(n)): # pylint: disable=unused-variable
257 if n > 0:
258 t2 = datetime(year=t2.year, month=t2.month, day=1) + timedelta(days=32)
259 else:
260 t2 = datetime(year=t2.year, month=t2.month, day=1) - timedelta(days=2)
261 try:
262 t2 = t.replace(year=t2.year, month=t2.month)
263 except Exception:
264 last_day = monthrange(t2.year, t2.month)[1]
265 t2 = t.replace(year=t2.year, month=t2.month, day=last_day)
266 return t2
269def per_delta(start: datetime, end: datetime, delta: timedelta):
270 """
271 Iterates over time range in steps specified in delta.
273 :param start: Start of time range (inclusive)
274 :param end: End of time range (exclusive)
275 :param delta: Step interval
277 :return: Iterable collection of [(start+td*0, start+td*1), (start+td*1, start+td*2), ..., end)
278 """
279 curr = start
280 while curr < end:
281 curr_end = curr + delta
282 yield curr, curr_end
283 curr = curr_end
286def per_month(start: datetime, end: datetime, n: int = 1):
287 """
288 Iterates over time range in one month steps.
289 Clamps to number of days in given month.
291 :param start: Start of time range (inclusive)
292 :param end: End of time range (exclusive)
293 :param n: Number of months to step. Default is 1.
295 :return: Iterable collection of [(month+0, month+1), (month+1, month+2), ..., end)
296 """
297 curr = start.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
298 while curr < end:
299 curr_end = add_month(curr, n)
300 yield curr, curr_end
301 curr = curr_end
304def get_time_steps(step_type: str, begin: datetime, end: datetime) -> List[Tuple[datetime, datetime]]:
305 """
306 Returns time stamps by time step type [TIME_STEP_DAILY, TIME_STEP_WEEKLY, TIME_STEP_MONTHLY].
307 For example daily steps for a week returns 7 [begin, end) ranges for each day of the week.
308 :param step_type: One of TIME_STEP_DAILY, TIME_STEP_WEEKLY, TIME_STEP_MONTHLY
309 :param begin: datetime
310 :param end: datetime
311 :return: List of [begin, end), one for reach time step unit
312 """
313 after_end = end
314 if TIME_STEP_DAILY == step_type: 314 ↛ 316line 314 didn't jump to line 316, because the condition on line 314 was never false
315 after_end += timedelta(days=1)
316 elif TIME_STEP_WEEKLY == step_type:
317 after_end += timedelta(days=7)
318 elif TIME_STEP_MONTHLY == step_type:
319 after_end = add_month(end)
320 else:
321 raise ValueError('Time step "{}" not one of {}'.format(step_type, TIME_STEP_TYPES))
323 begins: List[datetime] = []
324 t0 = t = begin
325 n = 1
326 while t < after_end:
327 begins.append(t)
328 if step_type == TIME_STEP_DAILY: 328 ↛ 330line 328 didn't jump to line 330, because the condition on line 328 was never false
329 t = t0 + timedelta(days=n)
330 elif step_type == TIME_STEP_WEEKLY:
331 t = t0 + timedelta(days=7 * n)
332 elif step_type == TIME_STEP_MONTHLY:
333 t = add_month(t0, n)
334 n += 1
335 return [(begins[i], begins[i + 1]) for i in range(len(begins) - 1)]