Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/aniso8601/duration.py: 89%
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 import compat
7from aniso8601.builders import TupleBuilder
8from aniso8601.builders.python import PythonTimeBuilder
9from aniso8601.date import parse_date
10from aniso8601.decimalfraction import normalize
11from aniso8601.exceptions import ISOFormatError
12from aniso8601.resolution import DurationResolution
13from aniso8601.time import parse_time
16def get_duration_resolution(isodurationstr):
17 # Valid string formats are:
18 #
19 # PnYnMnDTnHnMnS (or any reduced precision equivalent)
20 # PnW
21 # P<date>T<time>
22 isodurationtuple = parse_duration(isodurationstr, builder=TupleBuilder)
24 if isodurationtuple.TnS is not None:
25 return DurationResolution.Seconds
27 if isodurationtuple.TnM is not None:
28 return DurationResolution.Minutes
30 if isodurationtuple.TnH is not None:
31 return DurationResolution.Hours
33 if isodurationtuple.PnD is not None:
34 return DurationResolution.Days
36 if isodurationtuple.PnW is not None:
37 return DurationResolution.Weeks
39 if isodurationtuple.PnM is not None:
40 return DurationResolution.Months
42 return DurationResolution.Years
45def parse_duration(isodurationstr, builder=PythonTimeBuilder):
46 # Given a string representing an ISO 8601 duration, return a
47 # a duration built by the given builder. Valid formats are:
48 #
49 # PnYnMnDTnHnMnS (or any reduced precision equivalent)
50 # PnW
51 # P<date>T<time>
53 if compat.is_string(isodurationstr) is False:
54 raise ValueError("Duration must be string.")
56 if len(isodurationstr) == 0:
57 raise ISOFormatError(
58 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
59 )
61 if isodurationstr[0] != "P":
62 raise ISOFormatError("ISO 8601 duration must start with a P.")
64 # If Y, M, D, H, S, or W are in the string,
65 # assume it is a specified duration
66 if _has_any_component(isodurationstr, ["Y", "M", "D", "H", "S", "W"]) is True:
67 parseresult = _parse_duration_prescribed(isodurationstr)
68 return builder.build_duration(**parseresult)
70 if isodurationstr.find("T") != -1:
71 parseresult = _parse_duration_combined(isodurationstr)
72 return builder.build_duration(**parseresult)
74 raise ISOFormatError(
75 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
76 )
79def _parse_duration_prescribed(isodurationstr):
80 # durationstr can be of the form PnYnMnDTnHnMnS or PnW
82 # Make sure the end character is valid
83 # https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed
84 if isodurationstr[-1] not in ["Y", "M", "D", "H", "S", "W"]:
85 raise ISOFormatError("ISO 8601 duration must end with a valid character.")
87 # Make sure only the lowest order element has decimal precision
88 durationstr = normalize(isodurationstr)
90 if durationstr.count(".") > 1:
91 raise ISOFormatError(
92 "ISO 8601 allows only lowest order element to have a decimal fraction."
93 )
95 seperatoridx = durationstr.find(".")
97 if seperatoridx != -1:
98 remaining = durationstr[seperatoridx + 1 : -1]
100 # There should only ever be 1 letter after a decimal if there is more
101 # then one, the string is invalid
102 if remaining.isdigit() is False:
103 raise ISOFormatError(
104 "ISO 8601 duration must end with a single valid character."
105 )
107 # Do not allow W in combination with other designators
108 # https://bitbucket.org/nielsenb/aniso8601/issues/2/week-designators-should-not-be-combinable
109 if (
110 durationstr.find("W") != -1
111 and _has_any_component(durationstr, ["Y", "M", "D", "H", "S"]) is True
112 ):
113 raise ISOFormatError(
114 "ISO 8601 week designators may not be combined "
115 "with other time designators."
116 )
118 # Parse the elements of the duration
119 if durationstr.find("T") == -1:
120 return _parse_duration_prescribed_notime(durationstr)
122 return _parse_duration_prescribed_time(durationstr)
125def _parse_duration_prescribed_notime(isodurationstr):
126 # durationstr can be of the form PnYnMnD or PnW
128 durationstr = normalize(isodurationstr)
130 yearstr = None
131 monthstr = None
132 daystr = None
133 weekstr = None
135 weekidx = durationstr.find("W")
136 yearidx = durationstr.find("Y")
137 monthidx = durationstr.find("M")
138 dayidx = durationstr.find("D")
140 if weekidx != -1:
141 weekstr = durationstr[1:-1]
142 elif yearidx != -1 and monthidx != -1 and dayidx != -1:
143 yearstr = durationstr[1:yearidx]
144 monthstr = durationstr[yearidx + 1 : monthidx]
145 daystr = durationstr[monthidx + 1 : -1]
146 elif yearidx != -1 and monthidx != -1:
147 yearstr = durationstr[1:yearidx]
148 monthstr = durationstr[yearidx + 1 : monthidx]
149 elif yearidx != -1 and dayidx != -1:
150 yearstr = durationstr[1:yearidx]
151 daystr = durationstr[yearidx + 1 : dayidx]
152 elif monthidx != -1 and dayidx != -1:
153 monthstr = durationstr[1:monthidx]
154 daystr = durationstr[monthidx + 1 : -1]
155 elif yearidx != -1:
156 yearstr = durationstr[1:-1]
157 elif monthidx != -1:
158 monthstr = durationstr[1:-1]
159 elif dayidx != -1:
160 daystr = durationstr[1:-1]
161 else:
162 raise ISOFormatError(
163 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
164 )
166 for componentstr in [yearstr, monthstr, daystr, weekstr]:
167 if componentstr is not None:
168 if "." in componentstr:
169 intstr = componentstr.split(".", 1)[0]
171 if intstr.isdigit() is False:
172 raise ISOFormatError(
173 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
174 )
175 else:
176 if componentstr.isdigit() is False:
177 raise ISOFormatError(
178 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
179 )
181 return {"PnY": yearstr, "PnM": monthstr, "PnW": weekstr, "PnD": daystr}
184def _parse_duration_prescribed_time(isodurationstr):
185 # durationstr can be of the form PnYnMnDTnHnMnS
187 timeidx = isodurationstr.find("T")
189 datestr = isodurationstr[:timeidx]
190 timestr = normalize(isodurationstr[timeidx + 1 :])
192 hourstr = None
193 minutestr = None
194 secondstr = None
196 houridx = timestr.find("H")
197 minuteidx = timestr.find("M")
198 secondidx = timestr.find("S")
200 if houridx != -1 and minuteidx != -1 and secondidx != -1:
201 hourstr = timestr[0:houridx]
202 minutestr = timestr[houridx + 1 : minuteidx]
203 secondstr = timestr[minuteidx + 1 : -1]
204 elif houridx != -1 and minuteidx != -1:
205 hourstr = timestr[0:houridx]
206 minutestr = timestr[houridx + 1 : minuteidx]
207 elif houridx != -1 and secondidx != -1:
208 hourstr = timestr[0:houridx]
209 secondstr = timestr[houridx + 1 : -1]
210 elif minuteidx != -1 and secondidx != -1:
211 minutestr = timestr[0:minuteidx]
212 secondstr = timestr[minuteidx + 1 : -1]
213 elif houridx != -1:
214 hourstr = timestr[0:-1]
215 elif minuteidx != -1:
216 minutestr = timestr[0:-1]
217 elif secondidx != -1:
218 secondstr = timestr[0:-1]
219 else:
220 raise ISOFormatError(
221 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
222 )
224 for componentstr in [hourstr, minutestr, secondstr]:
225 if componentstr is not None:
226 if "." in componentstr:
227 intstr = componentstr.split(".", 1)[0]
229 if intstr.isdigit() is False:
230 raise ISOFormatError(
231 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
232 )
233 else:
234 if componentstr.isdigit() is False:
235 raise ISOFormatError(
236 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)
237 )
239 # Parse any date components
240 durationdict = {"PnY": None, "PnM": None, "PnW": None, "PnD": None}
242 if len(datestr) > 1:
243 durationdict = _parse_duration_prescribed_notime(datestr)
245 durationdict.update({"TnH": hourstr, "TnM": minutestr, "TnS": secondstr})
247 return durationdict
250def _parse_duration_combined(durationstr):
251 # Period of the form P<date>T<time>
253 # Split the string in to its component parts
254 datepart, timepart = durationstr[1:].split("T", 1) # We skip the 'P'
256 datevalue = parse_date(datepart, builder=TupleBuilder)
257 timevalue = parse_time(timepart, builder=TupleBuilder)
259 return {
260 "PnY": datevalue.YYYY,
261 "PnM": datevalue.MM,
262 "PnD": datevalue.DD,
263 "TnH": timevalue.hh,
264 "TnM": timevalue.mm,
265 "TnS": timevalue.ss,
266 }
269def _has_any_component(durationstr, components):
270 # Given a duration string, and a list of components, returns True
271 # if any of the listed components are present, False otherwise.
272 #
273 # For instance:
274 # durationstr = 'P1Y'
275 # components = ['Y', 'M']
276 #
277 # returns True
278 #
279 # durationstr = 'P1Y'
280 # components = ['M', 'D']
281 #
282 # returns False
284 for component in components:
285 if durationstr.find(component) != -1:
286 return True
288 return False