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)