Coverage for jutil/dates.py : 74%

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]
19# plus +- date ranges from current datetime:
20# (e.g. --yesterday is full day yesterday but --prev-1d is 24h less from current time)
21for d in [90, 60, 45, 30, 15, 7, 2, 1]:
22 TIME_RANGE_CHOICES.extend([
23 ('prev_{}d'.format(d), format_lazy('-{} {}', d, _('number.of.days'))),
24 ('plus_minus_{}d'.format(d), format_lazy('+-{} {}', d, _('number.of.days'))),
25 ('next_{}d'.format(d), format_lazy('+{} {}', d, _('number.of.days'))),
26 ])
28TIME_RANGE_NAMES = list(zip(*TIME_RANGE_CHOICES))[0]
30TIME_STEP_DAILY = 'daily'
31TIME_STEP_WEEKLY = 'weekly'
32TIME_STEP_MONTHLY = 'monthly'
34TIME_STEP_TYPES = [
35 TIME_STEP_DAILY,
36 TIME_STEP_WEEKLY,
37 TIME_STEP_MONTHLY,
38]
40TIME_STEP_CHOICES = [
41 (TIME_STEP_DAILY, _('daily')),
42 (TIME_STEP_WEEKLY, _('weekly')),
43 (TIME_STEP_MONTHLY, _('monthly')),
44]
46TIME_STEP_NAMES = list(zip(*TIME_STEP_CHOICES))[0]
49def localize_time_range(begin: datetime, end: datetime, tz: Any = None) -> Tuple[datetime, datetime]:
50 """
51 Localizes time range. Uses pytz.utc if None provided.
52 :param begin: Begin datetime
53 :param end: End datetime
54 :param tz: pytz timezone or None (default UTC)
55 :return: begin, end
56 """
57 if tz is None: 57 ↛ 59line 57 didn't jump to line 59, because the condition on line 57 was never false
58 tz = pytz.utc
59 return tz.localize(begin), tz.localize(end)
62def get_last_day_of_month(t: datetime) -> int:
63 """
64 Returns day number of the last day of the month
65 :param t: datetime
66 :return: int
67 """
68 tn = t + timedelta(days=32)
69 tn = datetime(year=tn.year, month=tn.month, day=1)
70 tt = tn - timedelta(hours=1)
71 return tt.day
74def end_of_month(today: Optional[datetime] = None, n: int = 0, tz: Any = None) -> datetime:
75 """
76 Returns end-of-month (last microsecond) of given datetime (or current datetime UTC if no parameter is passed).
77 :param today: Some date in the month (defaults current datetime)
78 :param n: +- number of months to offset from current month. Default 0.
79 :param tz: Timezone (defaults pytz UTC)
80 :return: datetime
81 """
82 if today is None: 82 ↛ 83line 82 didn't jump to line 83, because the condition on line 82 was never true
83 today = datetime.utcnow()
84 last_day = monthrange(today.year, today.month)[1]
85 end = today.replace(day=last_day, hour=0, minute=0, second=0, microsecond=0) + timedelta(hours=24)
86 while n > 0:
87 last_day = monthrange(end.year, end.month)[1]
88 end = end.replace(day=last_day, hour=0, minute=0, second=0, microsecond=0) + timedelta(hours=24)
89 n -= 1
90 while n < 0:
91 end -= timedelta(days=1)
92 end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
93 n += 1
94 end_incl = end - timedelta(microseconds=1)
95 if tz is None: 95 ↛ 96line 95 didn't jump to line 96, because the condition on line 95 was never true
96 tz = pytz.utc
97 return tz.localize(end_incl)
100def this_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
101 """
102 Returns this week begin (inclusive) and end (exclusive).
103 Week is assumed to start from Monday (ISO).
104 :param today: Some date (defaults current datetime)
105 :param tz: Timezone (defaults pytz UTC)
106 :return: begin (inclusive), end (exclusive)
107 """
108 if today is None: 108 ↛ 109line 108 didn't jump to line 109, because the condition on line 108 was never true
109 today = datetime.utcnow()
110 begin = today - timedelta(days=today.weekday())
111 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
112 return localize_time_range(begin, begin + timedelta(days=7), tz)
115def this_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
116 """
117 Returns current month begin (inclusive) and end (exclusive).
118 :param today: Some date in the month (defaults current datetime)
119 :param tz: Timezone (defaults pytz UTC)
120 :return: begin (inclusive), end (exclusive)
121 """
122 if today is None: 122 ↛ 123line 122 didn't jump to line 123, because the condition on line 122 was never true
123 today = datetime.utcnow()
124 begin = datetime(day=1, month=today.month, year=today.year)
125 end = begin + timedelta(days=32)
126 end = datetime(day=1, month=end.month, year=end.year)
127 return localize_time_range(begin, end, tz)
130def this_year(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
131 """
132 Returns this year begin (inclusive) and end (exclusive).
133 :param today: Some date (defaults current datetime)
134 :param tz: Timezone (defaults pytz UTC)
135 :return: begin (inclusive), end (exclusive)
136 """
137 if today is None: 137 ↛ 138line 137 didn't jump to line 138, because the condition on line 137 was never true
138 today = datetime.utcnow()
139 begin = datetime(day=1, month=1, year=today.year)
140 next_year = today + timedelta(days=365)
141 end = datetime(day=1, month=1, year=next_year.year)
142 return localize_time_range(begin, end, tz)
145def next_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
146 """
147 Returns next week begin (inclusive) and end (exclusive).
148 :param today: Some date (defaults current datetime)
149 :param tz: Timezone (defaults pytz UTC)
150 :return: begin (inclusive), end (exclusive)
151 """
152 if today is None: 152 ↛ 153line 152 didn't jump to line 153, because the condition on line 152 was never true
153 today = datetime.utcnow()
154 begin = today + timedelta(days=7-today.weekday())
155 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
156 return localize_time_range(begin, begin + timedelta(days=7), tz)
159def next_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
160 """
161 Returns next month begin (inclusive) and end (exclusive).
162 :param today: Some date in the month (defaults current datetime)
163 :param tz: Timezone (defaults pytz UTC)
164 :return: begin (inclusive), end (exclusive)
165 """
166 if today is None:
167 today = datetime.utcnow()
168 begin = datetime(day=1, month=today.month, year=today.year)
169 next_mo = begin + timedelta(days=32)
170 begin = datetime(day=1, month=next_mo.month, year=next_mo.year)
171 following_mo = begin + timedelta(days=32)
172 end = datetime(day=1, month=following_mo.month, year=following_mo.year)
173 return localize_time_range(begin, end, tz)
176def last_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
177 """
178 Returns last week begin (inclusive) and end (exclusive).
179 :param today: Some date (defaults current datetime)
180 :param tz: Timezone (defaults pytz UTC)
181 :return: begin (inclusive), end (exclusive)
182 """
183 if today is None: 183 ↛ 184line 183 didn't jump to line 184, because the condition on line 183 was never true
184 today = datetime.utcnow()
185 begin = today - timedelta(weeks=1, days=today.weekday())
186 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
187 return localize_time_range(begin, begin + timedelta(days=7), tz)
190def last_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
191 """
192 Returns last month begin (inclusive) and end (exclusive).
193 :param today: Some date (defaults current datetime)
194 :param tz: Timezone (defaults pytz UTC)
195 :return: begin (inclusive), end (exclusive)
196 """
197 if today is None: 197 ↛ 198line 197 didn't jump to line 198, because the condition on line 197 was never true
198 today = datetime.utcnow()
199 end = datetime(day=1, month=today.month, year=today.year)
200 end_incl = end - timedelta(seconds=1)
201 begin = datetime(day=1, month=end_incl.month, year=end_incl.year)
202 return localize_time_range(begin, end, tz)
205def last_year(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
206 """
207 Returns last year begin (inclusive) and end (exclusive).
208 :param today: Some date (defaults current datetime)
209 :param tz: Timezone (defaults pytz UTC)
210 :return: begin (inclusive), end (exclusive)
211 """
212 if today is None: 212 ↛ 213line 212 didn't jump to line 213, because the condition on line 212 was never true
213 today = datetime.utcnow()
214 end = datetime(day=1, month=1, year=today.year)
215 end_incl = end - timedelta(seconds=1)
216 begin = datetime(day=1, month=1, year=end_incl.year)
217 return localize_time_range(begin, end, tz)
220def yesterday(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
221 """
222 Returns yesterday begin (inclusive) and end (exclusive).
223 :param today: Some date (defaults current datetime)
224 :param tz: Timezone (defaults pytz UTC)
225 :return: begin (inclusive), end (exclusive)
226 """
227 if today is None: 227 ↛ 228line 227 didn't jump to line 228, because the condition on line 227 was never true
228 today = datetime.utcnow()
229 end = datetime(day=today.day, month=today.month, year=today.year)
230 end_incl = end - timedelta(seconds=1)
231 begin = datetime(day=end_incl.day, month=end_incl.month, year=end_incl.year)
232 return localize_time_range(begin, end, tz)
235def add_month(t: datetime, n: int = 1) -> datetime:
236 """
237 Adds +- n months to datetime.
238 Clamps days to number of days in given month.
239 :param t: datetime
240 :param n: +- number of months to offset from current month. Default 1.
241 :return: datetime
242 """
243 t2 = t
244 for count in range(abs(n)): # pylint: disable=unused-variable
245 if n > 0:
246 t2 = datetime(year=t2.year, month=t2.month, day=1) + timedelta(days=32)
247 else:
248 t2 = datetime(year=t2.year, month=t2.month, day=1) - timedelta(days=2)
249 try:
250 t2 = t.replace(year=t2.year, month=t2.month)
251 except Exception:
252 last_day = monthrange(t2.year, t2.month)[1]
253 t2 = t.replace(year=t2.year, month=t2.month, day=last_day)
254 return t2
257def per_delta(start: datetime, end: datetime, delta: timedelta):
258 """
259 Iterates over time range in steps specified in delta.
261 :param start: Start of time range (inclusive)
262 :param end: End of time range (exclusive)
263 :param delta: Step interval
265 :return: Iterable collection of [(start+td*0, start+td*1), (start+td*1, start+td*2), ..., end)
266 """
267 curr = start
268 while curr < end:
269 curr_end = curr + delta
270 yield curr, curr_end
271 curr = curr_end
274def per_month(start: datetime, end: datetime, n: int = 1):
275 """
276 Iterates over time range in one month steps.
277 Clamps to number of days in given month.
279 :param start: Start of time range (inclusive)
280 :param end: End of time range (exclusive)
281 :param n: Number of months to step. Default is 1.
283 :return: Iterable collection of [(month+0, month+1), (month+1, month+2), ..., end)
284 """
285 curr = start.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
286 while curr < end:
287 curr_end = add_month(curr, n)
288 yield curr, curr_end
289 curr = curr_end
292def get_time_steps(step_type: str, begin: datetime, end: datetime) -> List[Tuple[datetime, datetime]]:
293 """
294 Returns time stamps by time step type [TIME_STEP_DAILY, TIME_STEP_WEEKLY, TIME_STEP_MONTHLY].
295 For example daily steps for a week returns 7 [begin, end) ranges for each day of the week.
296 :param step_type: One of TIME_STEP_DAILY, TIME_STEP_WEEKLY, TIME_STEP_MONTHLY
297 :param begin: datetime
298 :param end: datetime
299 :return: List of [begin, end), one for reach time step unit
300 """
301 after_end = end
302 if TIME_STEP_DAILY == step_type: 302 ↛ 304line 302 didn't jump to line 304, because the condition on line 302 was never false
303 after_end += timedelta(days=1)
304 elif TIME_STEP_WEEKLY == step_type:
305 after_end += timedelta(days=7)
306 elif TIME_STEP_MONTHLY == step_type:
307 after_end = add_month(end)
308 else:
309 raise ValueError('Time step "{}" not one of {}'.format(step_type, TIME_STEP_TYPES))
311 begins: List[datetime] = []
312 t0 = t = begin
313 n = 1
314 while t < after_end:
315 begins.append(t)
316 if step_type == TIME_STEP_DAILY: 316 ↛ 318line 316 didn't jump to line 318, because the condition on line 316 was never false
317 t = t0 + timedelta(days=n)
318 elif step_type == TIME_STEP_WEEKLY:
319 t = t0 + timedelta(days=7*n)
320 elif step_type == TIME_STEP_MONTHLY:
321 t = add_month(t0, n)
322 n += 1
323 return [(begins[i], begins[i+1]) for i in range(len(begins)-1)]