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
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"""
2This module defines a Duration class.
4The class Duration allows to define durations in years and months and can be
5used as limited replacement for timedelta objects.
6"""
8from datetime import timedelta
9from decimal import ROUND_FLOOR, Decimal
12def fquotmod(val, low, high):
13 """
14 A divmod function with boundaries.
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
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
42class Duration:
43 """
44 A class which represents a duration.
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.
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.
58 A Duration can also be converted into a datetime object, but this requires
59 a start date or an end date.
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 """
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 )
90 def __getstate__(self):
91 return self.__dict__
93 def __setstate__(self, state):
94 self.__dict__.update(state)
96 def __getattr__(self, name):
97 """
98 Provide direct access to attributes of included timedelta instance.
99 """
100 return getattr(self.tdelta, name)
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)
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 )
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))
138 def __neg__(self):
139 """
140 A simple unary minus.
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
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
195 __radd__ = __add__
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
204 __rmul__ = __mul__
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
227 def __rsub__(self, other):
228 """
229 It is possible to subtract Duration objects from date, datetime and
230 timedelta objects.
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
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
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
303 def totimedelta(self, start=None, end=None):
304 """
305 Convert this duration into a timedelta object.
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)