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] 

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 ) 

29 

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

31 

32TIME_STEP_DAILY = "daily" 

33TIME_STEP_WEEKLY = "weekly" 

34TIME_STEP_MONTHLY = "monthly" 

35 

36TIME_STEP_TYPES = [ 

37 TIME_STEP_DAILY, 

38 TIME_STEP_WEEKLY, 

39 TIME_STEP_MONTHLY, 

40] 

41 

42TIME_STEP_CHOICES = [ 

43 (TIME_STEP_DAILY, _("daily")), 

44 (TIME_STEP_WEEKLY, _("weekly")), 

45 (TIME_STEP_MONTHLY, _("monthly")), 

46] 

47 

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

49 

50 

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) 

62 

63 

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 

74 

75 

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) 

100 

101 

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) 

115 

116 

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) 

130 

131 

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) 

145 

146 

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) 

159 

160 

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) 

176 

177 

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) 

190 

191 

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) 

205 

206 

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) 

220 

221 

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) 

235 

236 

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 

257 

258 

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

260 """ 

261 Iterates over time range in steps specified in delta. 

262 

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

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

265 :param delta: Step interval 

266 

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 

274 

275 

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. 

280 

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. 

284 

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 

292 

293 

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

312 

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