Hide keyboard shortcuts

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 _ 

7 

8 

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] 

34 

35TIME_RANGE_NAMES = list(zip(*TIME_RANGE_CHOICES))[0] 

36 

37TIME_STEP_DAILY = 'daily' 

38TIME_STEP_WEEKLY = 'weekly' 

39TIME_STEP_MONTHLY = 'monthly' 

40 

41TIME_STEP_TYPES = [ 

42 TIME_STEP_DAILY, 

43 TIME_STEP_WEEKLY, 

44 TIME_STEP_MONTHLY, 

45] 

46 

47TIME_STEP_CHOICES = [ 

48 (TIME_STEP_DAILY, _('daily')), 

49 (TIME_STEP_WEEKLY, _('weekly')), 

50 (TIME_STEP_MONTHLY, _('monthly')), 

51] 

52 

53TIME_STEP_NAMES = list(zip(*TIME_STEP_CHOICES))[0] 

54 

55 

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) 

67 

68 

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 

79 

80 

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) 

105 

106 

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) 

120 

121 

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) 

135 

136 

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) 

150 

151 

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) 

164 

165 

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) 

181 

182 

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) 

195 

196 

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) 

210 

211 

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) 

225 

226 

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) 

240 

241 

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 

262 

263 

264def per_delta(start: datetime, end: datetime, delta: timedelta): 

265 """ 

266 Iterates over time range in steps specified in delta. 

267 

268 :param start: Start of time range (inclusive) 

269 :param end: End of time range (exclusive) 

270 :param delta: Step interval 

271 

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 

279 

280 

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. 

285 

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. 

289 

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 

297 

298 

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)) 

317 

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)]