Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/aniso8601/interval.py: 60%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

146 statements  

1# -*- coding: utf-8 -*- 

2 

3# Copyright (c) 2026, Brandon Nielsen 

4# SPDX-License-Identifier: BSD-3-Clause 

5 

6from aniso8601.builders import DatetimeTuple, DateTuple, TupleBuilder 

7from aniso8601.builders.python import PythonTimeBuilder 

8from aniso8601.compat import is_string 

9from aniso8601.date import parse_date 

10from aniso8601.duration import parse_duration 

11from aniso8601.exceptions import ISOFormatError 

12from aniso8601.resolution import IntervalResolution 

13from aniso8601.time import parse_datetime, parse_time 

14 

15 

16def get_interval_resolution( 

17 isointervalstr, intervaldelimiter="/", datetimedelimiter="T" 

18): 

19 isointervaltuple = parse_interval( 

20 isointervalstr, 

21 intervaldelimiter=intervaldelimiter, 

22 datetimedelimiter=datetimedelimiter, 

23 builder=TupleBuilder, 

24 ) 

25 

26 return _get_interval_resolution(isointervaltuple) 

27 

28 

29def get_repeating_interval_resolution( 

30 isointervalstr, intervaldelimiter="/", datetimedelimiter="T" 

31): 

32 repeatingintervaltuple = parse_repeating_interval( 

33 isointervalstr, 

34 intervaldelimiter=intervaldelimiter, 

35 datetimedelimiter=datetimedelimiter, 

36 builder=TupleBuilder, 

37 ) 

38 

39 return _get_interval_resolution(repeatingintervaltuple.interval) 

40 

41 

42def _get_interval_resolution(intervaltuple): 

43 if intervaltuple.start is not None and intervaltuple.end is not None: 

44 return max( 

45 _get_interval_component_resolution(intervaltuple.start), 

46 _get_interval_component_resolution(intervaltuple.end), 

47 ) 

48 

49 if intervaltuple.start is not None and intervaltuple.duration is not None: 

50 return max( 

51 _get_interval_component_resolution(intervaltuple.start), 

52 _get_interval_component_resolution(intervaltuple.duration), 

53 ) 

54 

55 return max( 

56 _get_interval_component_resolution(intervaltuple.end), 

57 _get_interval_component_resolution(intervaltuple.duration), 

58 ) 

59 

60 

61def _get_interval_component_resolution(componenttuple): 

62 if isinstance(componenttuple, DateTuple): 

63 if componenttuple.DDD is not None: 

64 # YYYY-DDD 

65 # YYYYDDD 

66 return IntervalResolution.Ordinal 

67 

68 if componenttuple.D is not None: 

69 # YYYY-Www-D 

70 # YYYYWwwD 

71 return IntervalResolution.Weekday 

72 

73 if componenttuple.Www is not None: 

74 # YYYY-Www 

75 # YYYYWww 

76 return IntervalResolution.Week 

77 

78 if componenttuple.DD is not None: 

79 # YYYY-MM-DD 

80 # YYYYMMDD 

81 return IntervalResolution.Day 

82 

83 if componenttuple.MM is not None: 

84 # YYYY-MM 

85 return IntervalResolution.Month 

86 

87 # Y[YYY] 

88 return IntervalResolution.Year 

89 

90 if isinstance(componenttuple, DatetimeTuple): 

91 # Datetime 

92 if componenttuple.time.ss is not None: 

93 return IntervalResolution.Seconds 

94 

95 if componenttuple.time.mm is not None: 

96 return IntervalResolution.Minutes 

97 

98 return IntervalResolution.Hours 

99 

100 # Duration 

101 if componenttuple.TnS is not None: 

102 return IntervalResolution.Seconds 

103 

104 if componenttuple.TnM is not None: 

105 return IntervalResolution.Minutes 

106 

107 if componenttuple.TnH is not None: 

108 return IntervalResolution.Hours 

109 

110 if componenttuple.PnD is not None: 

111 return IntervalResolution.Day 

112 

113 if componenttuple.PnW is not None: 

114 return IntervalResolution.Week 

115 

116 if componenttuple.PnM is not None: 

117 return IntervalResolution.Month 

118 

119 return IntervalResolution.Year 

120 

121 

122def parse_interval( 

123 isointervalstr, 

124 intervaldelimiter="/", 

125 datetimedelimiter="T", 

126 builder=PythonTimeBuilder, 

127): 

128 # Given a string representing an ISO 8601 interval, return an 

129 # interval built by the given builder. Valid formats are: 

130 # 

131 # <start>/<end> 

132 # <start>/<duration> 

133 # <duration>/<end> 

134 # 

135 # The <start> and <end> values can represent dates, or datetimes, 

136 # not times. 

137 # 

138 # The format: 

139 # 

140 # <duration> 

141 # 

142 # Is expressly not supported as there is no way to provide the additional 

143 # required context. 

144 

145 if is_string(isointervalstr) is False: 

146 raise ValueError("Interval must be string.") 

147 

148 if len(isointervalstr) == 0: 

149 raise ISOFormatError("Interval string is empty.") 

150 

151 if isointervalstr[0] == "R": 

152 raise ISOFormatError( 

153 "ISO 8601 repeating intervals must be parsed " 

154 "with parse_repeating_interval." 

155 ) 

156 

157 intervaldelimitercount = isointervalstr.count(intervaldelimiter) 

158 

159 if intervaldelimitercount == 0: 

160 raise ISOFormatError( 

161 'Interval delimiter "{0}" is not in interval ' 

162 'string "{1}".'.format(intervaldelimiter, isointervalstr) 

163 ) 

164 

165 if intervaldelimitercount > 1: 

166 raise ISOFormatError( 

167 "{0} is not a valid ISO 8601 interval".format(isointervalstr) 

168 ) 

169 

170 return _parse_interval( 

171 isointervalstr, builder, intervaldelimiter, datetimedelimiter 

172 ) 

173 

174 

175def parse_repeating_interval( 

176 isointervalstr, 

177 intervaldelimiter="/", 

178 datetimedelimiter="T", 

179 builder=PythonTimeBuilder, 

180): 

181 # Given a string representing an ISO 8601 interval repeating, return an 

182 # interval built by the given builder. Valid formats are: 

183 # 

184 # Rnn/<interval> 

185 # R/<interval> 

186 

187 if not isinstance(isointervalstr, str): 

188 raise ValueError("Interval must be string.") 

189 

190 if len(isointervalstr) == 0: 

191 raise ISOFormatError("Repeating interval string is empty.") 

192 

193 if isointervalstr[0] != "R": 

194 raise ISOFormatError("ISO 8601 repeating interval must start with an R.") 

195 

196 if intervaldelimiter not in isointervalstr: 

197 raise ISOFormatError( 

198 'Interval delimiter "{0}" is not in interval ' 

199 'string "{1}".'.format(intervaldelimiter, isointervalstr) 

200 ) 

201 

202 # Parse the number of iterations 

203 iterationpart, intervalpart = isointervalstr.split(intervaldelimiter, 1) 

204 

205 if len(iterationpart) > 1: 

206 R = False 

207 Rnn = iterationpart[1:] 

208 else: 

209 R = True 

210 Rnn = None 

211 

212 interval = _parse_interval( 

213 intervalpart, TupleBuilder, intervaldelimiter, datetimedelimiter 

214 ) 

215 

216 return builder.build_repeating_interval(R=R, Rnn=Rnn, interval=interval) 

217 

218 

219def _parse_interval( 

220 isointervalstr, builder, intervaldelimiter="/", datetimedelimiter="T" 

221): 

222 # Returns a tuple containing the start of the interval, the end of the 

223 # interval, and or the interval duration 

224 

225 firstpart, secondpart = isointervalstr.split(intervaldelimiter) 

226 

227 if len(firstpart) == 0 or len(secondpart) == 0: 

228 raise ISOFormatError( 

229 "{0} is not a valid ISO 8601 interval".format(isointervalstr) 

230 ) 

231 

232 if firstpart[0] == "P": 

233 # <duration>/<end> 

234 # Notice that these are not returned 'in order' (earlier to later), this 

235 # is to maintain consistency with parsing <start>/<end> durations, as 

236 # well as making repeating interval code cleaner. Users who desire 

237 # durations to be in order can use the 'sorted' operator. 

238 duration = parse_duration(firstpart, builder=TupleBuilder) 

239 

240 # We need to figure out if <end> is a date, or a datetime 

241 if secondpart.find(datetimedelimiter) != -1: 

242 # <end> is a datetime 

243 endtuple = parse_datetime( 

244 secondpart, delimiter=datetimedelimiter, builder=TupleBuilder 

245 ) 

246 else: 

247 endtuple = parse_date(secondpart, builder=TupleBuilder) 

248 

249 return builder.build_interval(end=endtuple, duration=duration) 

250 

251 if secondpart[0] == "P": 

252 # <start>/<duration> 

253 # We need to figure out if <start> is a date, or a datetime 

254 duration = parse_duration(secondpart, builder=TupleBuilder) 

255 

256 if firstpart.find(datetimedelimiter) != -1: 

257 # <start> is a datetime 

258 starttuple = parse_datetime( 

259 firstpart, delimiter=datetimedelimiter, builder=TupleBuilder 

260 ) 

261 else: 

262 # <start> must just be a date 

263 starttuple = parse_date(firstpart, builder=TupleBuilder) 

264 

265 return builder.build_interval(start=starttuple, duration=duration) 

266 

267 # <start>/<end> 

268 if firstpart.find(datetimedelimiter) != -1: 

269 # Both parts are datetimes 

270 starttuple = parse_datetime( 

271 firstpart, delimiter=datetimedelimiter, builder=TupleBuilder 

272 ) 

273 else: 

274 starttuple = parse_date(firstpart, builder=TupleBuilder) 

275 

276 endtuple = _parse_interval_end(secondpart, starttuple, datetimedelimiter) 

277 

278 return builder.build_interval(start=starttuple, end=endtuple) 

279 

280 

281def _parse_interval_end(endstr, starttuple, datetimedelimiter): 

282 datestr = None 

283 timestr = None 

284 

285 monthstr = None 

286 daystr = None 

287 

288 concise = False 

289 

290 if isinstance(starttuple, DateTuple): 

291 startdatetuple = starttuple 

292 else: 

293 # Start is a datetime 

294 startdatetuple = starttuple.date 

295 

296 if datetimedelimiter in endstr: 

297 datestr, timestr = endstr.split(datetimedelimiter, 1) 

298 elif ":" in endstr: 

299 timestr = endstr 

300 else: 

301 datestr = endstr 

302 

303 if timestr is not None: 

304 endtimetuple = parse_time(timestr, builder=TupleBuilder) 

305 

306 # End is just a time 

307 if datestr is None: 

308 return endtimetuple 

309 

310 # Handle backwards concise representation 

311 if datestr.count("-") == 1: 

312 monthstr, daystr = datestr.split("-") 

313 concise = True 

314 elif len(datestr) <= 2: 

315 daystr = datestr 

316 concise = True 

317 elif len(datestr) <= 4: 

318 monthstr = datestr[0:2] 

319 daystr = datestr[2:] 

320 concise = True 

321 

322 if concise is True: 

323 concisedatestr = startdatetuple.YYYY 

324 

325 # Separators required because concise elements may be missing digits 

326 if monthstr is not None: 

327 concisedatestr += "-" + monthstr 

328 else: 

329 concisedatestr += "-" + startdatetuple.MM 

330 

331 concisedatestr += "-" + daystr 

332 

333 enddatetuple = parse_date(concisedatestr, builder=TupleBuilder) 

334 

335 # Clear unsupplied components 

336 if monthstr is None: 

337 enddatetuple = TupleBuilder.build_date(DD=enddatetuple.DD) 

338 else: 

339 # Year not provided 

340 enddatetuple = TupleBuilder.build_date( 

341 MM=enddatetuple.MM, DD=enddatetuple.DD 

342 ) 

343 else: 

344 enddatetuple = parse_date(datestr, builder=TupleBuilder) 

345 

346 if timestr is None: 

347 return enddatetuple 

348 

349 return TupleBuilder.build_datetime(enddatetuple, endtimetuple)