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