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

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) 2025, Brandon Nielsen 

4# All rights reserved. 

5# 

6# This software may be modified and distributed under the terms 

7# of the BSD license. See the LICENSE file for details. 

8 

9from aniso8601.builders import DatetimeTuple, DateTuple, TupleBuilder 

10from aniso8601.builders.python import PythonTimeBuilder 

11from aniso8601.compat import is_string 

12from aniso8601.date import parse_date 

13from aniso8601.duration import parse_duration 

14from aniso8601.exceptions import ISOFormatError 

15from aniso8601.resolution import IntervalResolution 

16from aniso8601.time import parse_datetime, parse_time 

17 

18 

19def get_interval_resolution( 

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

21): 

22 isointervaltuple = parse_interval( 

23 isointervalstr, 

24 intervaldelimiter=intervaldelimiter, 

25 datetimedelimiter=datetimedelimiter, 

26 builder=TupleBuilder, 

27 ) 

28 

29 return _get_interval_resolution(isointervaltuple) 

30 

31 

32def get_repeating_interval_resolution( 

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

34): 

35 repeatingintervaltuple = parse_repeating_interval( 

36 isointervalstr, 

37 intervaldelimiter=intervaldelimiter, 

38 datetimedelimiter=datetimedelimiter, 

39 builder=TupleBuilder, 

40 ) 

41 

42 return _get_interval_resolution(repeatingintervaltuple.interval) 

43 

44 

45def _get_interval_resolution(intervaltuple): 

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

47 return max( 

48 _get_interval_component_resolution(intervaltuple.start), 

49 _get_interval_component_resolution(intervaltuple.end), 

50 ) 

51 

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

53 return max( 

54 _get_interval_component_resolution(intervaltuple.start), 

55 _get_interval_component_resolution(intervaltuple.duration), 

56 ) 

57 

58 return max( 

59 _get_interval_component_resolution(intervaltuple.end), 

60 _get_interval_component_resolution(intervaltuple.duration), 

61 ) 

62 

63 

64def _get_interval_component_resolution(componenttuple): 

65 if isinstance(componenttuple, DateTuple): 

66 if componenttuple.DDD is not None: 

67 # YYYY-DDD 

68 # YYYYDDD 

69 return IntervalResolution.Ordinal 

70 

71 if componenttuple.D is not None: 

72 # YYYY-Www-D 

73 # YYYYWwwD 

74 return IntervalResolution.Weekday 

75 

76 if componenttuple.Www is not None: 

77 # YYYY-Www 

78 # YYYYWww 

79 return IntervalResolution.Week 

80 

81 if componenttuple.DD is not None: 

82 # YYYY-MM-DD 

83 # YYYYMMDD 

84 return IntervalResolution.Day 

85 

86 if componenttuple.MM is not None: 

87 # YYYY-MM 

88 return IntervalResolution.Month 

89 

90 # Y[YYY] 

91 return IntervalResolution.Year 

92 

93 if isinstance(componenttuple, DatetimeTuple): 

94 # Datetime 

95 if componenttuple.time.ss is not None: 

96 return IntervalResolution.Seconds 

97 

98 if componenttuple.time.mm is not None: 

99 return IntervalResolution.Minutes 

100 

101 return IntervalResolution.Hours 

102 

103 # Duration 

104 if componenttuple.TnS is not None: 

105 return IntervalResolution.Seconds 

106 

107 if componenttuple.TnM is not None: 

108 return IntervalResolution.Minutes 

109 

110 if componenttuple.TnH is not None: 

111 return IntervalResolution.Hours 

112 

113 if componenttuple.PnD is not None: 

114 return IntervalResolution.Day 

115 

116 if componenttuple.PnW is not None: 

117 return IntervalResolution.Week 

118 

119 if componenttuple.PnM is not None: 

120 return IntervalResolution.Month 

121 

122 return IntervalResolution.Year 

123 

124 

125def parse_interval( 

126 isointervalstr, 

127 intervaldelimiter="/", 

128 datetimedelimiter="T", 

129 builder=PythonTimeBuilder, 

130): 

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

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

133 # 

134 # <start>/<end> 

135 # <start>/<duration> 

136 # <duration>/<end> 

137 # 

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

139 # not times. 

140 # 

141 # The format: 

142 # 

143 # <duration> 

144 # 

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

146 # required context. 

147 

148 if is_string(isointervalstr) is False: 

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

150 

151 if len(isointervalstr) == 0: 

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

153 

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

155 raise ISOFormatError( 

156 "ISO 8601 repeating intervals must be parsed " 

157 "with parse_repeating_interval." 

158 ) 

159 

160 intervaldelimitercount = isointervalstr.count(intervaldelimiter) 

161 

162 if intervaldelimitercount == 0: 

163 raise ISOFormatError( 

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

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

166 ) 

167 

168 if intervaldelimitercount > 1: 

169 raise ISOFormatError( 

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

171 ) 

172 

173 return _parse_interval( 

174 isointervalstr, builder, intervaldelimiter, datetimedelimiter 

175 ) 

176 

177 

178def parse_repeating_interval( 

179 isointervalstr, 

180 intervaldelimiter="/", 

181 datetimedelimiter="T", 

182 builder=PythonTimeBuilder, 

183): 

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

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

186 # 

187 # Rnn/<interval> 

188 # R/<interval> 

189 

190 if not isinstance(isointervalstr, str): 

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

192 

193 if len(isointervalstr) == 0: 

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

195 

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

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

198 

199 if intervaldelimiter not in isointervalstr: 

200 raise ISOFormatError( 

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

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

203 ) 

204 

205 # Parse the number of iterations 

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

207 

208 if len(iterationpart) > 1: 

209 R = False 

210 Rnn = iterationpart[1:] 

211 else: 

212 R = True 

213 Rnn = None 

214 

215 interval = _parse_interval( 

216 intervalpart, TupleBuilder, intervaldelimiter, datetimedelimiter 

217 ) 

218 

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

220 

221 

222def _parse_interval( 

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

224): 

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

226 # interval, and or the interval duration 

227 

228 firstpart, secondpart = isointervalstr.split(intervaldelimiter) 

229 

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

231 raise ISOFormatError( 

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

233 ) 

234 

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

236 # <duration>/<end> 

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

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

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

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

241 duration = parse_duration(firstpart, builder=TupleBuilder) 

242 

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

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

245 # <end> is a datetime 

246 endtuple = parse_datetime( 

247 secondpart, delimiter=datetimedelimiter, builder=TupleBuilder 

248 ) 

249 else: 

250 endtuple = parse_date(secondpart, builder=TupleBuilder) 

251 

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

253 

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

255 # <start>/<duration> 

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

257 duration = parse_duration(secondpart, builder=TupleBuilder) 

258 

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

260 # <start> is a datetime 

261 starttuple = parse_datetime( 

262 firstpart, delimiter=datetimedelimiter, builder=TupleBuilder 

263 ) 

264 else: 

265 # <start> must just be a date 

266 starttuple = parse_date(firstpart, builder=TupleBuilder) 

267 

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

269 

270 # <start>/<end> 

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

272 # Both parts are datetimes 

273 starttuple = parse_datetime( 

274 firstpart, delimiter=datetimedelimiter, builder=TupleBuilder 

275 ) 

276 else: 

277 starttuple = parse_date(firstpart, builder=TupleBuilder) 

278 

279 endtuple = _parse_interval_end(secondpart, starttuple, datetimedelimiter) 

280 

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

282 

283 

284def _parse_interval_end(endstr, starttuple, datetimedelimiter): 

285 datestr = None 

286 timestr = None 

287 

288 monthstr = None 

289 daystr = None 

290 

291 concise = False 

292 

293 if isinstance(starttuple, DateTuple): 

294 startdatetuple = starttuple 

295 else: 

296 # Start is a datetime 

297 startdatetuple = starttuple.date 

298 

299 if datetimedelimiter in endstr: 

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

301 elif ":" in endstr: 

302 timestr = endstr 

303 else: 

304 datestr = endstr 

305 

306 if timestr is not None: 

307 endtimetuple = parse_time(timestr, builder=TupleBuilder) 

308 

309 # End is just a time 

310 if datestr is None: 

311 return endtimetuple 

312 

313 # Handle backwards concise representation 

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

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

316 concise = True 

317 elif len(datestr) <= 2: 

318 daystr = datestr 

319 concise = True 

320 elif len(datestr) <= 4: 

321 monthstr = datestr[0:2] 

322 daystr = datestr[2:] 

323 concise = True 

324 

325 if concise is True: 

326 concisedatestr = startdatetuple.YYYY 

327 

328 # Separators required because concise elements may be missing digits 

329 if monthstr is not None: 

330 concisedatestr += "-" + monthstr 

331 else: 

332 concisedatestr += "-" + startdatetuple.MM 

333 

334 concisedatestr += "-" + daystr 

335 

336 enddatetuple = parse_date(concisedatestr, builder=TupleBuilder) 

337 

338 # Clear unsupplied components 

339 if monthstr is None: 

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

341 else: 

342 # Year not provided 

343 enddatetuple = TupleBuilder.build_date( 

344 MM=enddatetuple.MM, DD=enddatetuple.DD 

345 ) 

346 else: 

347 enddatetuple = parse_date(datestr, builder=TupleBuilder) 

348 

349 if timestr is None: 

350 return enddatetuple 

351 

352 return TupleBuilder.build_datetime(enddatetuple, endtimetuple)