Coverage for jutil/dates.py : 73%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 ('last_month', _('last month')),
11 ('last_year', _('last year')),
12 ('this_month', _('this month')),
13 ('this_year', _('this year')),
14 ('last_week', _('last week')),
15 ('this_week', _('this week')),
16 ('yesterday', _('yesterday')),
17 ('today', _('today')),
18 ('prev_90d', format_lazy('-90 {}', _('number.of.days'))),
19 ('plus_minus_90d', format_lazy('+-90 {}', _('number.of.days'))),
20 ('next_90d', format_lazy('+90 {}', _('number.of.days'))),
21 ('prev_60d', format_lazy('-60 {}', _('number.of.days'))),
22 ('plus_minus_60d', format_lazy('+-60 {}', _('number.of.days'))),
23 ('next_60d', format_lazy('+60 {}', _('number.of.days'))),
24 ('prev_30d', format_lazy('-30 {}', _('number.of.days'))),
25 ('plus_minus_30d', format_lazy('+-30 {}', _('number.of.days'))),
26 ('next_30d', format_lazy('+30 {}', _('number.of.days'))),
27 ('prev_15d', format_lazy('-15 {}', _('number.of.days'))),
28 ('plus_minus_15d', format_lazy('+-15 {}', _('number.of.days'))),
29 ('next_15d', format_lazy('+15 {}', _('number.of.days'))),
30 ('prev_7d', format_lazy('-7 {}', _('number.of.days'))),
31 ('plus_minus_7d', format_lazy('+-7 {}', _('number.of.days'))),
32 ('next_7d', format_lazy('+7 {}', _('number.of.days'))),
33]
35TIME_RANGE_NAMES = list(zip(*TIME_RANGE_CHOICES))[0]
37TIME_STEP_DAILY = 'daily'
38TIME_STEP_WEEKLY = 'weekly'
39TIME_STEP_MONTHLY = 'monthly'
41TIME_STEP_TYPES = [
42 TIME_STEP_DAILY,
43 TIME_STEP_WEEKLY,
44 TIME_STEP_MONTHLY,
45]
47TIME_STEP_CHOICES = [
48 (TIME_STEP_DAILY, _('daily')),
49 (TIME_STEP_WEEKLY, _('weekly')),
50 (TIME_STEP_MONTHLY, _('monthly')),
51]
53TIME_STEP_NAMES = list(zip(*TIME_STEP_CHOICES))[0]
56def localize_time_range(begin: datetime, end: datetime, tz: Any = None) -> Tuple[datetime, datetime]:
57 """
58 Localizes time range. Uses pytz.utc if None provided.
59 :param begin: Begin datetime
60 :param end: End datetime
61 :param tz: pytz timezone or None (default UTC)
62 :return: begin, end
63 """
64 if tz is None: 64 ↛ 66line 64 didn't jump to line 66, because the condition on line 64 was never false
65 tz = pytz.utc
66 return tz.localize(begin), tz.localize(end)
69def get_last_day_of_month(t: datetime) -> int:
70 """
71 Returns day number of the last day of the month
72 :param t: datetime
73 :return: int
74 """
75 tn = t + timedelta(days=32)
76 tn = datetime(year=tn.year, month=tn.month, day=1)
77 tt = tn - timedelta(hours=1)
78 return tt.day
81def end_of_month(today: Optional[datetime] = None, n: int = 0, tz: Any = None) -> datetime:
82 """
83 Returns end-of-month (last microsecond) of given datetime (or current datetime UTC if no parameter is passed).
84 :param today: Some date in the month (defaults current datetime)
85 :param n: +- number of months to offset from current month. Default 0.
86 :param tz: Timezone (defaults pytz UTC)
87 :return: datetime
88 """
89 if today is None: 89 ↛ 90line 89 didn't jump to line 90, because the condition on line 89 was never true
90 today = datetime.utcnow()
91 last_day = monthrange(today.year, today.month)[1]
92 end = today.replace(day=last_day, hour=0, minute=0, second=0, microsecond=0) + timedelta(hours=24)
93 while n > 0:
94 last_day = monthrange(end.year, end.month)[1]
95 end = end.replace(day=last_day, hour=0, minute=0, second=0, microsecond=0) + timedelta(hours=24)
96 n -= 1
97 while n < 0:
98 end -= timedelta(days=1)
99 end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
100 n += 1
101 end_incl = end - timedelta(microseconds=1)
102 if tz is None: 102 ↛ 103line 102 didn't jump to line 103, because the condition on line 102 was never true
103 tz = pytz.utc
104 return tz.localize(end_incl)
107def this_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
108 """
109 Returns this week begin (inclusive) and end (exclusive).
110 Week is assumed to start from Monday (ISO).
111 :param today: Some date (defaults current datetime)
112 :param tz: Timezone (defaults pytz UTC)
113 :return: begin (inclusive), end (exclusive)
114 """
115 if today is None: 115 ↛ 116line 115 didn't jump to line 116, because the condition on line 115 was never true
116 today = datetime.utcnow()
117 begin = today - timedelta(days=today.weekday())
118 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
119 return localize_time_range(begin, begin + timedelta(days=7), tz)
122def this_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
123 """
124 Returns current month begin (inclusive) and end (exclusive).
125 :param today: Some date in the month (defaults current datetime)
126 :param tz: Timezone (defaults pytz UTC)
127 :return: begin (inclusive), end (exclusive)
128 """
129 if today is None: 129 ↛ 130line 129 didn't jump to line 130, because the condition on line 129 was never true
130 today = datetime.utcnow()
131 begin = datetime(day=1, month=today.month, year=today.year)
132 end = begin + timedelta(days=32)
133 end = datetime(day=1, month=end.month, year=end.year)
134 return localize_time_range(begin, end, tz)
137def this_year(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
138 """
139 Returns this year begin (inclusive) and end (exclusive).
140 :param today: Some date (defaults current datetime)
141 :param tz: Timezone (defaults pytz UTC)
142 :return: begin (inclusive), end (exclusive)
143 """
144 if today is None: 144 ↛ 145line 144 didn't jump to line 145, because the condition on line 144 was never true
145 today = datetime.utcnow()
146 begin = datetime(day=1, month=1, year=today.year)
147 next_year = today + timedelta(days=365)
148 end = datetime(day=1, month=1, year=next_year.year)
149 return localize_time_range(begin, end, tz)
152def next_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
153 """
154 Returns next week begin (inclusive) and end (exclusive).
155 :param today: Some date (defaults current datetime)
156 :param tz: Timezone (defaults pytz UTC)
157 :return: begin (inclusive), end (exclusive)
158 """
159 if today is None: 159 ↛ 160line 159 didn't jump to line 160, because the condition on line 159 was never true
160 today = datetime.utcnow()
161 begin = today + timedelta(days=7-today.weekday())
162 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
163 return localize_time_range(begin, begin + timedelta(days=7), tz)
166def next_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
167 """
168 Returns next month begin (inclusive) and end (exclusive).
169 :param today: Some date in the month (defaults current datetime)
170 :param tz: Timezone (defaults pytz UTC)
171 :return: begin (inclusive), end (exclusive)
172 """
173 if today is None:
174 today = datetime.utcnow()
175 begin = datetime(day=1, month=today.month, year=today.year)
176 next_mo = begin + timedelta(days=32)
177 begin = datetime(day=1, month=next_mo.month, year=next_mo.year)
178 following_mo = begin + timedelta(days=32)
179 end = datetime(day=1, month=following_mo.month, year=following_mo.year)
180 return localize_time_range(begin, end, tz)
183def last_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
184 """
185 Returns last week begin (inclusive) and end (exclusive).
186 :param today: Some date (defaults current datetime)
187 :param tz: Timezone (defaults pytz UTC)
188 :return: begin (inclusive), end (exclusive)
189 """
190 if today is None: 190 ↛ 191line 190 didn't jump to line 191, because the condition on line 190 was never true
191 today = datetime.utcnow()
192 begin = today - timedelta(weeks=1, days=today.weekday())
193 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
194 return localize_time_range(begin, begin + timedelta(days=7), tz)
197def last_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
198 """
199 Returns last month begin (inclusive) and end (exclusive).
200 :param today: Some date (defaults current datetime)
201 :param tz: Timezone (defaults pytz UTC)
202 :return: begin (inclusive), end (exclusive)
203 """
204 if today is None: 204 ↛ 205line 204 didn't jump to line 205, because the condition on line 204 was never true
205 today = datetime.utcnow()
206 end = datetime(day=1, month=today.month, year=today.year)
207 end_incl = end - timedelta(seconds=1)
208 begin = datetime(day=1, month=end_incl.month, year=end_incl.year)
209 return localize_time_range(begin, end, tz)
212def last_year(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
213 """
214 Returns last year begin (inclusive) and end (exclusive).
215 :param today: Some date (defaults current datetime)
216 :param tz: Timezone (defaults pytz UTC)
217 :return: begin (inclusive), end (exclusive)
218 """
219 if today is None: 219 ↛ 220line 219 didn't jump to line 220, because the condition on line 219 was never true
220 today = datetime.utcnow()
221 end = datetime(day=1, month=1, year=today.year)
222 end_incl = end - timedelta(seconds=1)
223 begin = datetime(day=1, month=1, year=end_incl.year)
224 return localize_time_range(begin, end, tz)
227def yesterday(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
228 """
229 Returns yesterday begin (inclusive) and end (exclusive).
230 :param today: Some date (defaults current datetime)
231 :param tz: Timezone (defaults pytz UTC)
232 :return: begin (inclusive), end (exclusive)
233 """
234 if today is None: 234 ↛ 235line 234 didn't jump to line 235, because the condition on line 234 was never true
235 today = datetime.utcnow()
236 end = datetime(day=today.day, month=today.month, year=today.year)
237 end_incl = end - timedelta(seconds=1)
238 begin = datetime(day=end_incl.day, month=end_incl.month, year=end_incl.year)
239 return localize_time_range(begin, end, tz)
242def add_month(t: datetime, n: int = 1) -> datetime:
243 """
244 Adds +- n months to datetime.
245 Clamps days to number of days in given month.
246 :param t: datetime
247 :param n: +- number of months to offset from current month. Default 1.
248 :return: datetime
249 """
250 t2 = t
251 for count in range(abs(n)): # pylint: disable=unused-variable
252 if n > 0:
253 t2 = datetime(year=t2.year, month=t2.month, day=1) + timedelta(days=32)
254 else:
255 t2 = datetime(year=t2.year, month=t2.month, day=1) - timedelta(days=2)
256 try:
257 t2 = t.replace(year=t2.year, month=t2.month)
258 except Exception:
259 last_day = monthrange(t2.year, t2.month)[1]
260 t2 = t.replace(year=t2.year, month=t2.month, day=last_day)
261 return t2
264def per_delta(start: datetime, end: datetime, delta: timedelta):
265 """
266 Iterates over time range in steps specified in delta.
268 :param start: Start of time range (inclusive)
269 :param end: End of time range (exclusive)
270 :param delta: Step interval
272 :return: Iterable collection of [(start+td*0, start+td*1), (start+td*1, start+td*2), ..., end)
273 """
274 curr = start
275 while curr < end:
276 curr_end = curr + delta
277 yield curr, curr_end
278 curr = curr_end
281def per_month(start: datetime, end: datetime, n: int = 1):
282 """
283 Iterates over time range in one month steps.
284 Clamps to number of days in given month.
286 :param start: Start of time range (inclusive)
287 :param end: End of time range (exclusive)
288 :param n: Number of months to step. Default is 1.
290 :return: Iterable collection of [(month+0, month+1), (month+1, month+2), ..., end)
291 """
292 curr = start.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
293 while curr < end:
294 curr_end = add_month(curr, n)
295 yield curr, curr_end
296 curr = curr_end
299def get_time_steps(step_type: str, begin: datetime, end: datetime) -> List[Tuple[datetime, datetime]]:
300 """
301 Returns time stamps by time step type [TIME_STEP_DAILY, TIME_STEP_WEEKLY, TIME_STEP_MONTHLY].
302 For example daily steps for a week returns 7 [begin, end) ranges for each day of the week.
303 :param step_type: One of TIME_STEP_DAILY, TIME_STEP_WEEKLY, TIME_STEP_MONTHLY
304 :param begin: datetime
305 :param end: datetime
306 :return: List of [begin, end), one for reach time step unit
307 """
308 after_end = end
309 if TIME_STEP_DAILY == step_type: 309 ↛ 311line 309 didn't jump to line 311, because the condition on line 309 was never false
310 after_end += timedelta(days=1)
311 elif TIME_STEP_WEEKLY == step_type:
312 after_end += timedelta(days=7)
313 elif TIME_STEP_MONTHLY == step_type:
314 after_end = add_month(end)
315 else:
316 raise ValueError('Time step "{}" not one of {}'.format(step_type, TIME_STEP_TYPES))
318 begins: List[datetime] = []
319 t0 = t = begin
320 n = 1
321 while t < after_end:
322 begins.append(t)
323 if step_type == TIME_STEP_DAILY: 323 ↛ 325line 323 didn't jump to line 325, because the condition on line 323 was never false
324 t = t0 + timedelta(days=n)
325 elif step_type == TIME_STEP_WEEKLY:
326 t = t0 + timedelta(days=7*n)
327 elif step_type == TIME_STEP_MONTHLY:
328 t = add_month(t0, n)
329 n += 1
330 return [(begins[i], begins[i+1]) for i in range(len(begins)-1)]