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

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

141 statements  

1""" 

2This module defines a Duration class. 

3 

4The class Duration allows to define durations in years and months and can be 

5used as limited replacement for timedelta objects. 

6""" 

7 

8from datetime import timedelta 

9from decimal import ROUND_FLOOR, Decimal 

10 

11 

12def fquotmod(val, low, high): 

13 """ 

14 A divmod function with boundaries. 

15 

16 """ 

17 # assumes that all the maths is done with Decimals. 

18 # divmod for Decimal uses truncate instead of floor as builtin 

19 # divmod, so we have to do it manually here. 

20 a, b = val - low, high - low 

21 div = (a / b).to_integral(ROUND_FLOOR) 

22 mod = a - div * b 

23 # if we were not using Decimal, it would look like this. 

24 # div, mod = divmod(val - low, high - low) 

25 mod += low 

26 return int(div), mod 

27 

28 

29def max_days_in_month(year, month): 

30 """ 

31 Determines the number of days of a specific month in a specific year. 

32 """ 

33 if month in (1, 3, 5, 7, 8, 10, 12): 

34 return 31 

35 if month in (4, 6, 9, 11): 

36 return 30 

37 if ((year % 400) == 0) or ((year % 100) != 0) and ((year % 4) == 0): 

38 return 29 

39 return 28 

40 

41 

42class Duration: 

43 """ 

44 A class which represents a duration. 

45 

46 The difference to datetime.timedelta is, that this class handles also 

47 differences given in years and months. 

48 A Duration treats differences given in year, months separately from all 

49 other components. 

50 

51 A Duration can be used almost like any timedelta object, however there 

52 are some restrictions: 

53 * It is not really possible to compare Durations, because it is unclear, 

54 whether a duration of 1 year is bigger than 365 days or not. 

55 * Equality is only tested between the two (year, month vs. timedelta) 

56 basic components. 

57 

58 A Duration can also be converted into a datetime object, but this requires 

59 a start date or an end date. 

60 

61 The algorithm to add a duration to a date is defined at 

62 http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes 

63 """ 

64 

65 def __init__( 

66 self, 

67 days=0, 

68 seconds=0, 

69 microseconds=0, 

70 milliseconds=0, 

71 minutes=0, 

72 hours=0, 

73 weeks=0, 

74 months=0, 

75 years=0, 

76 ): 

77 """ 

78 Initialise this Duration instance with the given parameters. 

79 """ 

80 if not isinstance(months, Decimal): 

81 months = Decimal(str(months)) 

82 if not isinstance(years, Decimal): 

83 years = Decimal(str(years)) 

84 self.months = months 

85 self.years = years 

86 self.tdelta = timedelta( 

87 days, seconds, microseconds, milliseconds, minutes, hours, weeks 

88 ) 

89 

90 def __getstate__(self): 

91 return self.__dict__ 

92 

93 def __setstate__(self, state): 

94 self.__dict__.update(state) 

95 

96 def __getattr__(self, name): 

97 """ 

98 Provide direct access to attributes of included timedelta instance. 

99 """ 

100 return getattr(self.tdelta, name) 

101 

102 def __str__(self): 

103 """ 

104 Return a string representation of this duration similar to timedelta. 

105 """ 

106 params = [] 

107 if self.years: 

108 params.append("%d years" % self.years) 

109 if self.months: 

110 fmt = "%d months" 

111 if self.months <= 1: 

112 fmt = "%d month" 

113 params.append(fmt % self.months) 

114 params.append(str(self.tdelta)) 

115 return ", ".join(params) 

116 

117 def __repr__(self): 

118 """ 

119 Return a string suitable for repr(x) calls. 

120 """ 

121 return "%s.%s(%d, %d, %d, years=%d, months=%d)" % ( 

122 self.__class__.__module__, 

123 self.__class__.__name__, 

124 self.tdelta.days, 

125 self.tdelta.seconds, 

126 self.tdelta.microseconds, 

127 self.years, 

128 self.months, 

129 ) 

130 

131 def __hash__(self): 

132 """ 

133 Return a hash of this instance so that it can be used in, for 

134 example, dicts and sets. 

135 """ 

136 return hash((self.tdelta, self.months, self.years)) 

137 

138 def __neg__(self): 

139 """ 

140 A simple unary minus. 

141 

142 Returns a new Duration instance with all it's negated. 

143 """ 

144 negduration = Duration(years=-self.years, months=-self.months) 

145 negduration.tdelta = -self.tdelta 

146 return negduration 

147 

148 def __add__(self, other): 

149 """ 

150 Durations can be added with Duration, timedelta, date and datetime 

151 objects. 

152 """ 

153 if isinstance(other, Duration): 

154 newduration = Duration( 

155 years=self.years + other.years, months=self.months + other.months 

156 ) 

157 newduration.tdelta = self.tdelta + other.tdelta 

158 return newduration 

159 try: 

160 # try anything that looks like a date or datetime 

161 # 'other' has attributes year, month, day 

162 # and relies on 'timedelta + other' being implemented 

163 if not (float(self.years).is_integer() and float(self.months).is_integer()): 

164 raise ValueError( 

165 "fractional years or months not supported" " for date calculations" 

166 ) 

167 newmonth = other.month + self.months 

168 carry, newmonth = fquotmod(newmonth, 1, 13) 

169 newyear = other.year + self.years + carry 

170 maxdays = max_days_in_month(newyear, newmonth) 

171 if other.day > maxdays: 

172 newday = maxdays 

173 else: 

174 newday = other.day 

175 newdt = other.replace( 

176 year=int(newyear), month=int(newmonth), day=int(newday) 

177 ) 

178 # does a timedelta + date/datetime 

179 return self.tdelta + newdt 

180 except AttributeError: 

181 # other probably was not a date/datetime compatible object 

182 pass 

183 try: 

184 # try if other is a timedelta 

185 # relies on timedelta + timedelta supported 

186 newduration = Duration(years=self.years, months=self.months) 

187 newduration.tdelta = self.tdelta + other 

188 return newduration 

189 except AttributeError: 

190 # ignore ... other probably was not a timedelta compatible object 

191 pass 

192 # we have tried everything .... return a NotImplemented 

193 return NotImplemented 

194 

195 __radd__ = __add__ 

196 

197 def __mul__(self, other): 

198 if isinstance(other, int): 

199 newduration = Duration(years=self.years * other, months=self.months * other) 

200 newduration.tdelta = self.tdelta * other 

201 return newduration 

202 return NotImplemented 

203 

204 __rmul__ = __mul__ 

205 

206 def __sub__(self, other): 

207 """ 

208 It is possible to subtract Duration and timedelta objects from Duration 

209 objects. 

210 """ 

211 if isinstance(other, Duration): 

212 newduration = Duration( 

213 years=self.years - other.years, months=self.months - other.months 

214 ) 

215 newduration.tdelta = self.tdelta - other.tdelta 

216 return newduration 

217 try: 

218 # do maths with our timedelta object .... 

219 newduration = Duration(years=self.years, months=self.months) 

220 newduration.tdelta = self.tdelta - other 

221 return newduration 

222 except TypeError: 

223 # looks like timedelta - other is not implemented 

224 pass 

225 return NotImplemented 

226 

227 def __rsub__(self, other): 

228 """ 

229 It is possible to subtract Duration objects from date, datetime and 

230 timedelta objects. 

231 

232 TODO: there is some weird behaviour in date - timedelta ... 

233 if timedelta has seconds or microseconds set, then 

234 date - timedelta != date + (-timedelta) 

235 for now we follow this behaviour to avoid surprises when mixing 

236 timedeltas with Durations, but in case this ever changes in 

237 the stdlib we can just do: 

238 return -self + other 

239 instead of all the current code 

240 """ 

241 if isinstance(other, timedelta): 

242 tmpdur = Duration() 

243 tmpdur.tdelta = other 

244 return tmpdur - self 

245 try: 

246 # check if other behaves like a date/datetime object 

247 # does it have year, month, day and replace? 

248 if not (float(self.years).is_integer() and float(self.months).is_integer()): 

249 raise ValueError( 

250 "fractional years or months not supported" " for date calculations" 

251 ) 

252 newmonth = other.month - self.months 

253 carry, newmonth = fquotmod(newmonth, 1, 13) 

254 newyear = other.year - self.years + carry 

255 maxdays = max_days_in_month(newyear, newmonth) 

256 if other.day > maxdays: 

257 newday = maxdays 

258 else: 

259 newday = other.day 

260 newdt = other.replace( 

261 year=int(newyear), month=int(newmonth), day=int(newday) 

262 ) 

263 return newdt - self.tdelta 

264 except AttributeError: 

265 # other probably was not compatible with data/datetime 

266 pass 

267 return NotImplemented 

268 

269 def __eq__(self, other): 

270 """ 

271 If the years, month part and the timedelta part are both equal, then 

272 the two Durations are considered equal. 

273 """ 

274 if isinstance(other, Duration): 

275 if (self.years * 12 + self.months) == ( 

276 other.years * 12 + other.months 

277 ) and self.tdelta == other.tdelta: 

278 return True 

279 return False 

280 # check if other con be compared against timedelta object 

281 # will raise an AssertionError when optimisation is off 

282 if self.years == 0 and self.months == 0: 

283 return self.tdelta == other 

284 return False 

285 

286 def __ne__(self, other): 

287 """ 

288 If the years, month part or the timedelta part is not equal, then 

289 the two Durations are considered not equal. 

290 """ 

291 if isinstance(other, Duration): 

292 if (self.years * 12 + self.months) != ( 

293 other.years * 12 + other.months 

294 ) or self.tdelta != other.tdelta: 

295 return True 

296 return False 

297 # check if other can be compared against timedelta object 

298 # will raise an AssertionError when optimisation is off 

299 if self.years == 0 and self.months == 0: 

300 return self.tdelta != other 

301 return True 

302 

303 def totimedelta(self, start=None, end=None): 

304 """ 

305 Convert this duration into a timedelta object. 

306 

307 This method requires a start datetime or end datetimem, but raises 

308 an exception if both are given. 

309 """ 

310 if start is None and end is None: 

311 raise ValueError("start or end required") 

312 if start is not None and end is not None: 

313 raise ValueError("only start or end allowed") 

314 if start is not None: 

315 return (start + self) - start 

316 return end - (end - self)