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
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
1# -*- coding: utf-8 -*-
3# Copyright (c) 2026, Brandon Nielsen
4# SPDX-License-Identifier: BSD-3-Clause
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
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 )
26 return _get_interval_resolution(isointervaltuple)
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 )
39 return _get_interval_resolution(repeatingintervaltuple.interval)
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 )
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 )
55 return max(
56 _get_interval_component_resolution(intervaltuple.end),
57 _get_interval_component_resolution(intervaltuple.duration),
58 )
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
68 if componenttuple.D is not None:
69 # YYYY-Www-D
70 # YYYYWwwD
71 return IntervalResolution.Weekday
73 if componenttuple.Www is not None:
74 # YYYY-Www
75 # YYYYWww
76 return IntervalResolution.Week
78 if componenttuple.DD is not None:
79 # YYYY-MM-DD
80 # YYYYMMDD
81 return IntervalResolution.Day
83 if componenttuple.MM is not None:
84 # YYYY-MM
85 return IntervalResolution.Month
87 # Y[YYY]
88 return IntervalResolution.Year
90 if isinstance(componenttuple, DatetimeTuple):
91 # Datetime
92 if componenttuple.time.ss is not None:
93 return IntervalResolution.Seconds
95 if componenttuple.time.mm is not None:
96 return IntervalResolution.Minutes
98 return IntervalResolution.Hours
100 # Duration
101 if componenttuple.TnS is not None:
102 return IntervalResolution.Seconds
104 if componenttuple.TnM is not None:
105 return IntervalResolution.Minutes
107 if componenttuple.TnH is not None:
108 return IntervalResolution.Hours
110 if componenttuple.PnD is not None:
111 return IntervalResolution.Day
113 if componenttuple.PnW is not None:
114 return IntervalResolution.Week
116 if componenttuple.PnM is not None:
117 return IntervalResolution.Month
119 return IntervalResolution.Year
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.
145 if is_string(isointervalstr) is False:
146 raise ValueError("Interval must be string.")
148 if len(isointervalstr) == 0:
149 raise ISOFormatError("Interval string is empty.")
151 if isointervalstr[0] == "R":
152 raise ISOFormatError(
153 "ISO 8601 repeating intervals must be parsed "
154 "with parse_repeating_interval."
155 )
157 intervaldelimitercount = isointervalstr.count(intervaldelimiter)
159 if intervaldelimitercount == 0:
160 raise ISOFormatError(
161 'Interval delimiter "{0}" is not in interval '
162 'string "{1}".'.format(intervaldelimiter, isointervalstr)
163 )
165 if intervaldelimitercount > 1:
166 raise ISOFormatError(
167 "{0} is not a valid ISO 8601 interval".format(isointervalstr)
168 )
170 return _parse_interval(
171 isointervalstr, builder, intervaldelimiter, datetimedelimiter
172 )
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>
187 if not isinstance(isointervalstr, str):
188 raise ValueError("Interval must be string.")
190 if len(isointervalstr) == 0:
191 raise ISOFormatError("Repeating interval string is empty.")
193 if isointervalstr[0] != "R":
194 raise ISOFormatError("ISO 8601 repeating interval must start with an R.")
196 if intervaldelimiter not in isointervalstr:
197 raise ISOFormatError(
198 'Interval delimiter "{0}" is not in interval '
199 'string "{1}".'.format(intervaldelimiter, isointervalstr)
200 )
202 # Parse the number of iterations
203 iterationpart, intervalpart = isointervalstr.split(intervaldelimiter, 1)
205 if len(iterationpart) > 1:
206 R = False
207 Rnn = iterationpart[1:]
208 else:
209 R = True
210 Rnn = None
212 interval = _parse_interval(
213 intervalpart, TupleBuilder, intervaldelimiter, datetimedelimiter
214 )
216 return builder.build_repeating_interval(R=R, Rnn=Rnn, interval=interval)
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
225 firstpart, secondpart = isointervalstr.split(intervaldelimiter)
227 if len(firstpart) == 0 or len(secondpart) == 0:
228 raise ISOFormatError(
229 "{0} is not a valid ISO 8601 interval".format(isointervalstr)
230 )
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)
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)
249 return builder.build_interval(end=endtuple, duration=duration)
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)
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)
265 return builder.build_interval(start=starttuple, duration=duration)
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)
276 endtuple = _parse_interval_end(secondpart, starttuple, datetimedelimiter)
278 return builder.build_interval(start=starttuple, end=endtuple)
281def _parse_interval_end(endstr, starttuple, datetimedelimiter):
282 datestr = None
283 timestr = None
285 monthstr = None
286 daystr = None
288 concise = False
290 if isinstance(starttuple, DateTuple):
291 startdatetuple = starttuple
292 else:
293 # Start is a datetime
294 startdatetuple = starttuple.date
296 if datetimedelimiter in endstr:
297 datestr, timestr = endstr.split(datetimedelimiter, 1)
298 elif ":" in endstr:
299 timestr = endstr
300 else:
301 datestr = endstr
303 if timestr is not None:
304 endtimetuple = parse_time(timestr, builder=TupleBuilder)
306 # End is just a time
307 if datestr is None:
308 return endtimetuple
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
322 if concise is True:
323 concisedatestr = startdatetuple.YYYY
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
331 concisedatestr += "-" + daystr
333 enddatetuple = parse_date(concisedatestr, builder=TupleBuilder)
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)
346 if timestr is None:
347 return enddatetuple
349 return TupleBuilder.build_datetime(enddatetuple, endtimetuple)