Coverage for jutil/dates.py: 74%

# Coverage for jutil/dates.py : 74%

## 150 statements

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

1 from datetime import datetime, timedelta

2 from typing import Tuple, Any, Optional, List

3 import pytz

4 from calendar import monthrange

5 from django.utils.text import format_lazy

6 from django.utils.translation import gettext_lazy as _

7

8

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

21 for 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

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

31

32 TIME_STEP_DAILY = "daily"

33 TIME_STEP_WEEKLY = "weekly"

34 TIME_STEP_MONTHLY = "monthly"

35

36 TIME_STEP_TYPES = [

37 TIME_STEP_DAILY,

38 TIME_STEP_WEEKLY,

39 TIME_STEP_MONTHLY,

40]

41

42 TIME_STEP_CHOICES = [

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

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

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

46]

47

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

49

50

51 def 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

64 def 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

76 def 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

102 def 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

117 def 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

132 def 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

147 def 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

161 def 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

178 def 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

192 def 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

207 def 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

222 def 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

237 def 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

259 def 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

276 def 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

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