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

27 

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

29 

30TIME_STEP_DAILY = 'daily' 

31TIME_STEP_WEEKLY = 'weekly' 

32TIME_STEP_MONTHLY = 'monthly' 

33 

34TIME_STEP_TYPES = [ 

35 TIME_STEP_DAILY, 

36 TIME_STEP_WEEKLY, 

37 TIME_STEP_MONTHLY, 

38] 

39 

40TIME_STEP_CHOICES = [ 

41 (TIME_STEP_DAILY, _('daily')), 

42 (TIME_STEP_WEEKLY, _('weekly')), 

43 (TIME_STEP_MONTHLY, _('monthly')), 

44] 

45 

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

47 

48 

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) 

60 

61 

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 

72 

73 

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) 

98 

99 

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) 

113 

114 

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) 

128 

129 

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) 

143 

144 

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) 

157 

158 

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) 

174 

175 

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) 

188 

189 

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) 

203 

204 

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) 

218 

219 

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) 

233 

234 

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 

255 

256 

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

258 """ 

259 Iterates over time range in steps specified in delta. 

260 

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

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

263 :param delta: Step interval 

264 

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 

272 

273 

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. 

278 

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. 

282 

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 

290 

291 

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

310 

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