Coverage for jutil/dates.py : 78%

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
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 ('last_week', _('last week')),
14 ('yesterday', _('yesterday')),
15 ('today', _('today')),
16 ('prev_90d', format_lazy('-90 {}', _('number.of.days'))),
17 ('plus_minus_90d', format_lazy('+-90 {}', _('number.of.days'))),
18 ('next_90d', format_lazy('+90 {}', _('number.of.days'))),
19 ('prev_60d', format_lazy('-60 {}', _('number.of.days'))),
20 ('plus_minus_60d', format_lazy('+-60 {}', _('number.of.days'))),
21 ('next_60d', format_lazy('+60 {}', _('number.of.days'))),
22 ('prev_30d', format_lazy('-30 {}', _('number.of.days'))),
23 ('plus_minus_30d', format_lazy('+-30 {}', _('number.of.days'))),
24 ('next_30d', format_lazy('+30 {}', _('number.of.days'))),
25 ('prev_15d', format_lazy('-15 {}', _('number.of.days'))),
26 ('plus_minus_15d', format_lazy('+-15 {}', _('number.of.days'))),
27 ('next_15d', format_lazy('+15 {}', _('number.of.days'))),
28 ('prev_7d', format_lazy('-7 {}', _('number.of.days'))),
29 ('plus_minus_7d', format_lazy('+-7 {}', _('number.of.days'))),
30 ('next_7d', format_lazy('+7 {}', _('number.of.days'))),
31]
33TIME_STEP_CHOICES = [
34 ('daily', _('daily')),
35 ('weekly', _('weekly')),
36 ('monthly', _('monthly')),
37]
39TIME_RANGE_NAMES = list(zip(*TIME_RANGE_CHOICES))[0]
41TIME_STEP_NAMES = list(zip(*TIME_STEP_CHOICES))[0]
44def get_last_day_of_month(t: datetime) -> int:
45 """
46 Returns day number of the last day of the month
47 :param t: datetime
48 :return: int
49 """
50 tn = t + timedelta(days=32)
51 tn = datetime(year=tn.year, month=tn.month, day=1)
52 tt = tn - timedelta(hours=1)
53 return tt.day
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 this_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
70 """
71 Returns this week begin (inclusive) and end (exclusive).
72 :param today: Some date (defaults current datetime)
73 :param tz: Timezone (defaults pytz UTC)
74 :return: begin (inclusive), end (exclusive)
75 """
76 if today is None: 76 ↛ 77line 76 didn't jump to line 77, because the condition on line 76 was never true
77 today = datetime.utcnow()
78 begin = today - timedelta(days=today.weekday())
79 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
80 return localize_time_range(begin, begin + timedelta(days=7), tz)
83def this_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
84 """
85 Returns current month begin (inclusive) and end (exclusive).
86 :param today: Some date in the month (defaults current datetime)
87 :param tz: Timezone (defaults pytz UTC)
88 :return: begin (inclusive), end (exclusive)
89 """
90 if today is None: 90 ↛ 91line 90 didn't jump to line 91, because the condition on line 90 was never true
91 today = datetime.utcnow()
92 begin = datetime(day=1, month=today.month, year=today.year)
93 end = begin + timedelta(days=32)
94 end = datetime(day=1, month=end.month, year=end.year)
95 return localize_time_range(begin, end, tz)
98def end_of_month(today: Optional[datetime] = None, n: int = 0, tz: Any = None) -> datetime:
99 """
100 Returns end-of-month (last microsecond) of given datetime (or current datetime UTC if no parameter is passed).
101 :param today: Some date in the month (defaults current datetime)
102 :param n: +- number of months to offset from current month. Default 0.
103 :param tz: Timezone (defaults pytz UTC)
104 :return: datetime
105 """
106 if today is None: 106 ↛ 107line 106 didn't jump to line 107, because the condition on line 106 was never true
107 today = datetime.utcnow()
108 last_day = monthrange(today.year, today.month)[1]
109 end = today.replace(day=last_day, hour=0, minute=0, second=0, microsecond=0) + timedelta(hours=24)
110 while n > 0:
111 last_day = monthrange(end.year, end.month)[1]
112 end = end.replace(day=last_day, hour=0, minute=0, second=0, microsecond=0) + timedelta(hours=24)
113 n -= 1
114 while n < 0:
115 end -= timedelta(days=1)
116 end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
117 n += 1
118 end_incl = end - timedelta(microseconds=1)
119 if tz is None: 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true
120 tz = pytz.utc
121 return tz.localize(end_incl)
124def next_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
125 """
126 Returns next week begin (inclusive) and end (exclusive).
127 :param today: Some date (defaults current datetime)
128 :param tz: Timezone (defaults pytz UTC)
129 :return: begin (inclusive), end (exclusive)
130 """
131 if today is None: 131 ↛ 132line 131 didn't jump to line 132, because the condition on line 131 was never true
132 today = datetime.utcnow()
133 begin = today + timedelta(days=7-today.weekday())
134 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
135 return localize_time_range(begin, begin + timedelta(days=7), tz)
138def next_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
139 """
140 Returns next month begin (inclusive) and end (exclusive).
141 :param today: Some date in the month (defaults current datetime)
142 :param tz: Timezone (defaults pytz UTC)
143 :return: begin (inclusive), end (exclusive)
144 """
145 if today is None:
146 today = datetime.utcnow()
147 begin = datetime(day=1, month=today.month, year=today.year)
148 next_mo = begin + timedelta(days=32)
149 begin = datetime(day=1, month=next_mo.month, year=next_mo.year)
150 following_mo = begin + timedelta(days=32)
151 end = datetime(day=1, month=following_mo.month, year=following_mo.year)
152 return localize_time_range(begin, end, tz)
155def last_week(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
156 """
157 Returns last week begin (inclusive) and end (exclusive).
158 :param today: Some date (defaults current datetime)
159 :param tz: Timezone (defaults pytz UTC)
160 :return: begin (inclusive), end (exclusive)
161 """
162 if today is None: 162 ↛ 163line 162 didn't jump to line 163, because the condition on line 162 was never true
163 today = datetime.utcnow()
164 begin = today - timedelta(weeks=1, days=today.weekday())
165 begin = datetime(year=begin.year, month=begin.month, day=begin.day)
166 return localize_time_range(begin, begin + timedelta(days=7), tz)
169def last_month(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
170 """
171 Returns last month begin (inclusive) and end (exclusive).
172 :param today: Some date (defaults current datetime)
173 :param tz: Timezone (defaults pytz UTC)
174 :return: begin (inclusive), end (exclusive)
175 """
176 if today is None: 176 ↛ 177line 176 didn't jump to line 177, because the condition on line 176 was never true
177 today = datetime.utcnow()
178 end = datetime(day=1, month=today.month, year=today.year)
179 end_incl = end - timedelta(seconds=1)
180 begin = datetime(day=1, month=end_incl.month, year=end_incl.year)
181 return localize_time_range(begin, end, tz)
184def last_year(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
185 """
186 Returns last year begin (inclusive) and end (exclusive).
187 :param today: Some date (defaults current datetime)
188 :param tz: Timezone (defaults pytz UTC)
189 :return: begin (inclusive), end (exclusive)
190 """
191 if today is None: 191 ↛ 192line 191 didn't jump to line 192, because the condition on line 191 was never true
192 today = datetime.utcnow()
193 end = datetime(day=1, month=1, year=today.year)
194 end_incl = end - timedelta(seconds=1)
195 begin = datetime(day=1, month=1, year=end_incl.year)
196 return localize_time_range(begin, end, tz)
199def yesterday(today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
200 """
201 Returns yesterday begin (inclusive) and end (exclusive).
202 :param today: Some date (defaults current datetime)
203 :param tz: Timezone (defaults pytz UTC)
204 :return: begin (inclusive), end (exclusive)
205 """
206 if today is None: 206 ↛ 207line 206 didn't jump to line 207, because the condition on line 206 was never true
207 today = datetime.utcnow()
208 end = datetime(day=today.day, month=today.month, year=today.year)
209 end_incl = end - timedelta(seconds=1)
210 begin = datetime(day=end_incl.day, month=end_incl.month, year=end_incl.year)
211 return localize_time_range(begin, end, tz)
214def add_month(t: datetime, n: int = 1) -> datetime:
215 """
216 Adds +- n months to datetime.
217 Clamps days to number of days in given month.
218 :param t: datetime
219 :param n: +- number of months to offset from current month. Default 1.
220 :return: datetime
221 """
222 t2 = t
223 for count in range(abs(n)): # pylint: disable=unused-variable
224 if n > 0:
225 t2 = datetime(year=t2.year, month=t2.month, day=1) + timedelta(days=32)
226 else:
227 t2 = datetime(year=t2.year, month=t2.month, day=1) - timedelta(days=2)
228 try:
229 t2 = t.replace(year=t2.year, month=t2.month)
230 except Exception:
231 last_day = monthrange(t2.year, t2.month)[1]
232 t2 = t.replace(year=t2.year, month=t2.month, day=last_day)
233 return t2
236def per_delta(start: datetime, end: datetime, delta: timedelta):
237 """
238 Iterates over time range in steps specified in delta.
240 :param start: Start of time range (inclusive)
241 :param end: End of time range (exclusive)
242 :param delta: Step interval
244 :return: Iterable collection of [(start+td*0, start+td*1), (start+td*1, start+td*2), ..., end)
245 """
246 curr = start
247 while curr < end:
248 curr_end = curr + delta
249 yield curr, curr_end
250 curr = curr_end
253def per_month(start: datetime, end: datetime, n: int = 1):
254 """
255 Iterates over time range in one month steps.
256 Clamps to number of days in given month.
258 :param start: Start of time range (inclusive)
259 :param end: End of time range (exclusive)
260 :param n: Number of months to step. Default is 1.
262 :return: Iterable collection of [(month+0, month+1), (month+1, month+2), ..., end)
263 """
264 curr = start.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
265 while curr < end:
266 curr_end = add_month(curr, n)
267 yield curr, curr_end
268 curr = curr_end