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

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

148 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 import compat 

10from aniso8601.builders import TupleBuilder 

11from aniso8601.builders.python import PythonTimeBuilder 

12from aniso8601.date import parse_date 

13from aniso8601.decimalfraction import normalize 

14from aniso8601.exceptions import ISOFormatError 

15from aniso8601.resolution import DurationResolution 

16from aniso8601.time import parse_time 

17 

18 

19def get_duration_resolution(isodurationstr): 

20 # Valid string formats are: 

21 # 

22 # PnYnMnDTnHnMnS (or any reduced precision equivalent) 

23 # PnW 

24 # P<date>T<time> 

25 isodurationtuple = parse_duration(isodurationstr, builder=TupleBuilder) 

26 

27 if isodurationtuple.TnS is not None: 

28 return DurationResolution.Seconds 

29 

30 if isodurationtuple.TnM is not None: 

31 return DurationResolution.Minutes 

32 

33 if isodurationtuple.TnH is not None: 

34 return DurationResolution.Hours 

35 

36 if isodurationtuple.PnD is not None: 

37 return DurationResolution.Days 

38 

39 if isodurationtuple.PnW is not None: 

40 return DurationResolution.Weeks 

41 

42 if isodurationtuple.PnM is not None: 

43 return DurationResolution.Months 

44 

45 return DurationResolution.Years 

46 

47 

48def parse_duration(isodurationstr, builder=PythonTimeBuilder): 

49 # Given a string representing an ISO 8601 duration, return a 

50 # a duration built by the given builder. Valid formats are: 

51 # 

52 # PnYnMnDTnHnMnS (or any reduced precision equivalent) 

53 # PnW 

54 # P<date>T<time> 

55 

56 if compat.is_string(isodurationstr) is False: 

57 raise ValueError("Duration must be string.") 

58 

59 if len(isodurationstr) == 0: 

60 raise ISOFormatError( 

61 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) 

62 ) 

63 

64 if isodurationstr[0] != "P": 

65 raise ISOFormatError("ISO 8601 duration must start with a P.") 

66 

67 # If Y, M, D, H, S, or W are in the string, 

68 # assume it is a specified duration 

69 if _has_any_component(isodurationstr, ["Y", "M", "D", "H", "S", "W"]) is True: 

70 parseresult = _parse_duration_prescribed(isodurationstr) 

71 return builder.build_duration(**parseresult) 

72 

73 if isodurationstr.find("T") != -1: 

74 parseresult = _parse_duration_combined(isodurationstr) 

75 return builder.build_duration(**parseresult) 

76 

77 raise ISOFormatError( 

78 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) 

79 ) 

80 

81 

82def _parse_duration_prescribed(isodurationstr): 

83 # durationstr can be of the form PnYnMnDTnHnMnS or PnW 

84 

85 # Make sure the end character is valid 

86 # https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed 

87 if isodurationstr[-1] not in ["Y", "M", "D", "H", "S", "W"]: 

88 raise ISOFormatError("ISO 8601 duration must end with a valid character.") 

89 

90 # Make sure only the lowest order element has decimal precision 

91 durationstr = normalize(isodurationstr) 

92 

93 if durationstr.count(".") > 1: 

94 raise ISOFormatError( 

95 "ISO 8601 allows only lowest order element to have a decimal fraction." 

96 ) 

97 

98 seperatoridx = durationstr.find(".") 

99 

100 if seperatoridx != -1: 

101 remaining = durationstr[seperatoridx + 1 : -1] 

102 

103 # There should only ever be 1 letter after a decimal if there is more 

104 # then one, the string is invalid 

105 if remaining.isdigit() is False: 

106 raise ISOFormatError( 

107 "ISO 8601 duration must end with a single valid character." 

108 ) 

109 

110 # Do not allow W in combination with other designators 

111 # https://bitbucket.org/nielsenb/aniso8601/issues/2/week-designators-should-not-be-combinable 

112 if ( 

113 durationstr.find("W") != -1 

114 and _has_any_component(durationstr, ["Y", "M", "D", "H", "S"]) is True 

115 ): 

116 raise ISOFormatError( 

117 "ISO 8601 week designators may not be combined " 

118 "with other time designators." 

119 ) 

120 

121 # Parse the elements of the duration 

122 if durationstr.find("T") == -1: 

123 return _parse_duration_prescribed_notime(durationstr) 

124 

125 return _parse_duration_prescribed_time(durationstr) 

126 

127 

128def _parse_duration_prescribed_notime(isodurationstr): 

129 # durationstr can be of the form PnYnMnD or PnW 

130 

131 durationstr = normalize(isodurationstr) 

132 

133 yearstr = None 

134 monthstr = None 

135 daystr = None 

136 weekstr = None 

137 

138 weekidx = durationstr.find("W") 

139 yearidx = durationstr.find("Y") 

140 monthidx = durationstr.find("M") 

141 dayidx = durationstr.find("D") 

142 

143 if weekidx != -1: 

144 weekstr = durationstr[1:-1] 

145 elif yearidx != -1 and monthidx != -1 and dayidx != -1: 

146 yearstr = durationstr[1:yearidx] 

147 monthstr = durationstr[yearidx + 1 : monthidx] 

148 daystr = durationstr[monthidx + 1 : -1] 

149 elif yearidx != -1 and monthidx != -1: 

150 yearstr = durationstr[1:yearidx] 

151 monthstr = durationstr[yearidx + 1 : monthidx] 

152 elif yearidx != -1 and dayidx != -1: 

153 yearstr = durationstr[1:yearidx] 

154 daystr = durationstr[yearidx + 1 : dayidx] 

155 elif monthidx != -1 and dayidx != -1: 

156 monthstr = durationstr[1:monthidx] 

157 daystr = durationstr[monthidx + 1 : -1] 

158 elif yearidx != -1: 

159 yearstr = durationstr[1:-1] 

160 elif monthidx != -1: 

161 monthstr = durationstr[1:-1] 

162 elif dayidx != -1: 

163 daystr = durationstr[1:-1] 

164 else: 

165 raise ISOFormatError( 

166 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) 

167 ) 

168 

169 for componentstr in [yearstr, monthstr, daystr, weekstr]: 

170 if componentstr is not None: 

171 if "." in componentstr: 

172 intstr = componentstr.split(".", 1)[0] 

173 

174 if intstr.isdigit() is False: 

175 raise ISOFormatError( 

176 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) 

177 ) 

178 else: 

179 if componentstr.isdigit() is False: 

180 raise ISOFormatError( 

181 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) 

182 ) 

183 

184 return {"PnY": yearstr, "PnM": monthstr, "PnW": weekstr, "PnD": daystr} 

185 

186 

187def _parse_duration_prescribed_time(isodurationstr): 

188 # durationstr can be of the form PnYnMnDTnHnMnS 

189 

190 timeidx = isodurationstr.find("T") 

191 

192 datestr = isodurationstr[:timeidx] 

193 timestr = normalize(isodurationstr[timeidx + 1 :]) 

194 

195 hourstr = None 

196 minutestr = None 

197 secondstr = None 

198 

199 houridx = timestr.find("H") 

200 minuteidx = timestr.find("M") 

201 secondidx = timestr.find("S") 

202 

203 if houridx != -1 and minuteidx != -1 and secondidx != -1: 

204 hourstr = timestr[0:houridx] 

205 minutestr = timestr[houridx + 1 : minuteidx] 

206 secondstr = timestr[minuteidx + 1 : -1] 

207 elif houridx != -1 and minuteidx != -1: 

208 hourstr = timestr[0:houridx] 

209 minutestr = timestr[houridx + 1 : minuteidx] 

210 elif houridx != -1 and secondidx != -1: 

211 hourstr = timestr[0:houridx] 

212 secondstr = timestr[houridx + 1 : -1] 

213 elif minuteidx != -1 and secondidx != -1: 

214 minutestr = timestr[0:minuteidx] 

215 secondstr = timestr[minuteidx + 1 : -1] 

216 elif houridx != -1: 

217 hourstr = timestr[0:-1] 

218 elif minuteidx != -1: 

219 minutestr = timestr[0:-1] 

220 elif secondidx != -1: 

221 secondstr = timestr[0:-1] 

222 else: 

223 raise ISOFormatError( 

224 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) 

225 ) 

226 

227 for componentstr in [hourstr, minutestr, secondstr]: 

228 if componentstr is not None: 

229 if "." in componentstr: 

230 intstr = componentstr.split(".", 1)[0] 

231 

232 if intstr.isdigit() is False: 

233 raise ISOFormatError( 

234 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) 

235 ) 

236 else: 

237 if componentstr.isdigit() is False: 

238 raise ISOFormatError( 

239 '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) 

240 ) 

241 

242 # Parse any date components 

243 durationdict = {"PnY": None, "PnM": None, "PnW": None, "PnD": None} 

244 

245 if len(datestr) > 1: 

246 durationdict = _parse_duration_prescribed_notime(datestr) 

247 

248 durationdict.update({"TnH": hourstr, "TnM": minutestr, "TnS": secondstr}) 

249 

250 return durationdict 

251 

252 

253def _parse_duration_combined(durationstr): 

254 # Period of the form P<date>T<time> 

255 

256 # Split the string in to its component parts 

257 datepart, timepart = durationstr[1:].split("T", 1) # We skip the 'P' 

258 

259 datevalue = parse_date(datepart, builder=TupleBuilder) 

260 timevalue = parse_time(timepart, builder=TupleBuilder) 

261 

262 return { 

263 "PnY": datevalue.YYYY, 

264 "PnM": datevalue.MM, 

265 "PnD": datevalue.DD, 

266 "TnH": timevalue.hh, 

267 "TnM": timevalue.mm, 

268 "TnS": timevalue.ss, 

269 } 

270 

271 

272def _has_any_component(durationstr, components): 

273 # Given a duration string, and a list of components, returns True 

274 # if any of the listed components are present, False otherwise. 

275 # 

276 # For instance: 

277 # durationstr = 'P1Y' 

278 # components = ['Y', 'M'] 

279 # 

280 # returns True 

281 # 

282 # durationstr = 'P1Y' 

283 # components = ['M', 'D'] 

284 # 

285 # returns False 

286 

287 for component in components: 

288 if durationstr.find(component) != -1: 

289 return True 

290 

291 return False