1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2021, 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 type(componenttuple) is 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 elif type(componenttuple) is DatetimeTuple:
93 # Datetime
94 if componenttuple.time.ss is not None:
95 return IntervalResolution.Seconds
96
97 if componenttuple.time.mm is not None:
98 return IntervalResolution.Minutes
99
100 return IntervalResolution.Hours
101
102 # Duration
103 if componenttuple.TnS is not None:
104 return IntervalResolution.Seconds
105
106 if componenttuple.TnM is not None:
107 return IntervalResolution.Minutes
108
109 if componenttuple.TnH is not None:
110 return IntervalResolution.Hours
111
112 if componenttuple.PnD is not None:
113 return IntervalResolution.Day
114
115 if componenttuple.PnW is not None:
116 return IntervalResolution.Week
117
118 if componenttuple.PnM is not None:
119 return IntervalResolution.Month
120
121 return IntervalResolution.Year
122
123
124def parse_interval(
125 isointervalstr,
126 intervaldelimiter="/",
127 datetimedelimiter="T",
128 builder=PythonTimeBuilder,
129):
130 # Given a string representing an ISO 8601 interval, return an
131 # interval built by the given builder. Valid formats are:
132 #
133 # <start>/<end>
134 # <start>/<duration>
135 # <duration>/<end>
136 #
137 # The <start> and <end> values can represent dates, or datetimes,
138 # not times.
139 #
140 # The format:
141 #
142 # <duration>
143 #
144 # Is expressly not supported as there is no way to provide the additional
145 # required context.
146
147 if is_string(isointervalstr) is False:
148 raise ValueError("Interval must be string.")
149
150 if len(isointervalstr) == 0:
151 raise ISOFormatError("Interval string is empty.")
152
153 if isointervalstr[0] == "R":
154 raise ISOFormatError(
155 "ISO 8601 repeating intervals must be parsed "
156 "with parse_repeating_interval."
157 )
158
159 intervaldelimitercount = isointervalstr.count(intervaldelimiter)
160
161 if intervaldelimitercount == 0:
162 raise ISOFormatError(
163 'Interval delimiter "{0}" is not in interval '
164 'string "{1}".'.format(intervaldelimiter, isointervalstr)
165 )
166
167 if intervaldelimitercount > 1:
168 raise ISOFormatError(
169 "{0} is not a valid ISO 8601 interval".format(isointervalstr)
170 )
171
172 return _parse_interval(
173 isointervalstr, builder, intervaldelimiter, datetimedelimiter
174 )
175
176
177def parse_repeating_interval(
178 isointervalstr,
179 intervaldelimiter="/",
180 datetimedelimiter="T",
181 builder=PythonTimeBuilder,
182):
183 # Given a string representing an ISO 8601 interval repeating, return an
184 # interval built by the given builder. Valid formats are:
185 #
186 # Rnn/<interval>
187 # R/<interval>
188
189 if not isinstance(isointervalstr, str):
190 raise ValueError("Interval must be string.")
191
192 if len(isointervalstr) == 0:
193 raise ISOFormatError("Repeating interval string is empty.")
194
195 if isointervalstr[0] != "R":
196 raise ISOFormatError("ISO 8601 repeating interval must start " "with an R.")
197
198 if intervaldelimiter not in isointervalstr:
199 raise ISOFormatError(
200 'Interval delimiter "{0}" is not in interval '
201 'string "{1}".'.format(intervaldelimiter, isointervalstr)
202 )
203
204 # Parse the number of iterations
205 iterationpart, intervalpart = isointervalstr.split(intervaldelimiter, 1)
206
207 if len(iterationpart) > 1:
208 R = False
209 Rnn = iterationpart[1:]
210 else:
211 R = True
212 Rnn = None
213
214 interval = _parse_interval(
215 intervalpart, TupleBuilder, intervaldelimiter, datetimedelimiter
216 )
217
218 return builder.build_repeating_interval(R=R, Rnn=Rnn, interval=interval)
219
220
221def _parse_interval(
222 isointervalstr, builder, intervaldelimiter="/", datetimedelimiter="T"
223):
224 # Returns a tuple containing the start of the interval, the end of the
225 # interval, and or the interval duration
226
227 firstpart, secondpart = isointervalstr.split(intervaldelimiter)
228
229 if len(firstpart) == 0 or len(secondpart) == 0:
230 raise ISOFormatError(
231 "{0} is not a valid ISO 8601 interval".format(isointervalstr)
232 )
233
234 if firstpart[0] == "P":
235 # <duration>/<end>
236 # Notice that these are not returned 'in order' (earlier to later), this
237 # is to maintain consistency with parsing <start>/<end> durations, as
238 # well as making repeating interval code cleaner. Users who desire
239 # durations to be in order can use the 'sorted' operator.
240 duration = parse_duration(firstpart, builder=TupleBuilder)
241
242 # We need to figure out if <end> is a date, or a datetime
243 if secondpart.find(datetimedelimiter) != -1:
244 # <end> is a datetime
245 endtuple = parse_datetime(
246 secondpart, delimiter=datetimedelimiter, builder=TupleBuilder
247 )
248 else:
249 endtuple = parse_date(secondpart, builder=TupleBuilder)
250
251 return builder.build_interval(end=endtuple, duration=duration)
252 elif secondpart[0] == "P":
253 # <start>/<duration>
254 # We need to figure out if <start> is a date, or a datetime
255 duration = parse_duration(secondpart, builder=TupleBuilder)
256
257 if firstpart.find(datetimedelimiter) != -1:
258 # <start> is a datetime
259 starttuple = parse_datetime(
260 firstpart, delimiter=datetimedelimiter, builder=TupleBuilder
261 )
262 else:
263 # <start> must just be a date
264 starttuple = parse_date(firstpart, builder=TupleBuilder)
265
266 return builder.build_interval(start=starttuple, duration=duration)
267
268 # <start>/<end>
269 if firstpart.find(datetimedelimiter) != -1:
270 # Both parts are datetimes
271 starttuple = parse_datetime(
272 firstpart, delimiter=datetimedelimiter, builder=TupleBuilder
273 )
274 else:
275 starttuple = parse_date(firstpart, builder=TupleBuilder)
276
277 endtuple = _parse_interval_end(secondpart, starttuple, datetimedelimiter)
278
279 return builder.build_interval(start=starttuple, end=endtuple)
280
281
282def _parse_interval_end(endstr, starttuple, datetimedelimiter):
283 datestr = None
284 timestr = None
285
286 monthstr = None
287 daystr = None
288
289 concise = False
290
291 if type(starttuple) is DateTuple:
292 startdatetuple = starttuple
293 else:
294 # Start is a datetime
295 startdatetuple = starttuple.date
296
297 if datetimedelimiter in endstr:
298 datestr, timestr = endstr.split(datetimedelimiter, 1)
299 elif ":" in endstr:
300 timestr = endstr
301 else:
302 datestr = endstr
303
304 if timestr is not None:
305 endtimetuple = parse_time(timestr, builder=TupleBuilder)
306
307 # End is just a time
308 if datestr is None:
309 return endtimetuple
310
311 # Handle backwards concise representation
312 if datestr.count("-") == 1:
313 monthstr, daystr = datestr.split("-")
314 concise = True
315 elif len(datestr) <= 2:
316 daystr = datestr
317 concise = True
318 elif len(datestr) <= 4:
319 monthstr = datestr[0:2]
320 daystr = datestr[2:]
321 concise = True
322
323 if concise is True:
324 concisedatestr = startdatetuple.YYYY
325
326 # Separators required because concise elements may be missing digits
327 if monthstr is not None:
328 concisedatestr += "-" + monthstr
329 elif startdatetuple.MM is not None:
330 concisedatestr += "-" + startdatetuple.MM
331
332 concisedatestr += "-" + daystr
333
334 enddatetuple = parse_date(concisedatestr, builder=TupleBuilder)
335
336 # Clear unsupplied components
337 if monthstr is None:
338 enddatetuple = TupleBuilder.build_date(DD=enddatetuple.DD)
339 else:
340 # Year not provided
341 enddatetuple = TupleBuilder.build_date(
342 MM=enddatetuple.MM, DD=enddatetuple.DD
343 )
344 else:
345 enddatetuple = parse_date(datestr, builder=TupleBuilder)
346
347 if timestr is None:
348 return enddatetuple
349
350 return TupleBuilder.build_datetime(enddatetuple, endtimetuple)