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 

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

32 

33TIME_STEP_CHOICES = [ 

34 ('daily', _('daily')), 

35 ('weekly', _('weekly')), 

36 ('monthly', _('monthly')), 

37] 

38 

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

40 

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

42 

43 

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 

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

81 

82 

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) 

96 

97 

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) 

122 

123 

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) 

136 

137 

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) 

153 

154 

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) 

167 

168 

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) 

182 

183 

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) 

197 

198 

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) 

212 

213 

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 

234 

235 

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

237 """ 

238 Iterates over time range in steps specified in delta. 

239 

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

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

242 :param delta: Step interval 

243 

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 

251 

252 

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. 

257 

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. 

261 

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