Coverage for jutil/dates.py: 78%

# Coverage for jutil/dates.py : 78%

## 115 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

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

33 TIME_STEP_CHOICES = [

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

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

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

37]

38

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

40

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

42

43

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

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

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

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

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

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

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

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

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

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

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

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

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

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