| 1 | """ |
|---|
| 2 | Copyright (c) 2003-2005 Gustavo Niemeyer <gustavo@niemeyer.net> |
|---|
| 3 | |
|---|
| 4 | This module offers extensions to the standard python 2.3+ |
|---|
| 5 | datetime module. |
|---|
| 6 | """ |
|---|
| 7 | __author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" |
|---|
| 8 | __license__ = "PSF License" |
|---|
| 9 | |
|---|
| 10 | import datetime |
|---|
| 11 | import calendar |
|---|
| 12 | |
|---|
| 13 | __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] |
|---|
| 14 | |
|---|
| 15 | class weekday(object): |
|---|
| 16 | __slots__ = ["weekday", "n"] |
|---|
| 17 | |
|---|
| 18 | def __init__(self, weekday, n=None): |
|---|
| 19 | self.weekday = weekday |
|---|
| 20 | self.n = n |
|---|
| 21 | |
|---|
| 22 | def __call__(self, n): |
|---|
| 23 | if n == self.n: |
|---|
| 24 | return self |
|---|
| 25 | else: |
|---|
| 26 | return self.__class__(self.weekday, n) |
|---|
| 27 | |
|---|
| 28 | def __eq__(self, other): |
|---|
| 29 | try: |
|---|
| 30 | if self.weekday != other.weekday or self.n != other.n: |
|---|
| 31 | return False |
|---|
| 32 | except AttributeError: |
|---|
| 33 | return False |
|---|
| 34 | return True |
|---|
| 35 | |
|---|
| 36 | def __repr__(self): |
|---|
| 37 | s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] |
|---|
| 38 | if not self.n: |
|---|
| 39 | return s |
|---|
| 40 | else: |
|---|
| 41 | return "%s(%+d)" % (s, self.n) |
|---|
| 42 | |
|---|
| 43 | MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) |
|---|
| 44 | |
|---|
| 45 | class relativedelta: |
|---|
| 46 | """ |
|---|
| 47 | The relativedelta type is based on the specification of the excelent |
|---|
| 48 | work done by M.-A. Lemburg in his mx.DateTime extension. However, |
|---|
| 49 | notice that this type does *NOT* implement the same algorithm as |
|---|
| 50 | his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. |
|---|
| 51 | |
|---|
| 52 | There's two different ways to build a relativedelta instance. The |
|---|
| 53 | first one is passing it two date/datetime classes: |
|---|
| 54 | |
|---|
| 55 | relativedelta(datetime1, datetime2) |
|---|
| 56 | |
|---|
| 57 | And the other way is to use the following keyword arguments: |
|---|
| 58 | |
|---|
| 59 | year, month, day, hour, minute, second, microsecond: |
|---|
| 60 | Absolute information. |
|---|
| 61 | |
|---|
| 62 | years, months, weeks, days, hours, minutes, seconds, microseconds: |
|---|
| 63 | Relative information, may be negative. |
|---|
| 64 | |
|---|
| 65 | weekday: |
|---|
| 66 | One of the weekday instances (MO, TU, etc). These instances may |
|---|
| 67 | receive a parameter N, specifying the Nth weekday, which could |
|---|
| 68 | be positive or negative (like MO(+1) or MO(-2). Not specifying |
|---|
| 69 | it is the same as specifying +1. You can also use an integer, |
|---|
| 70 | where 0=MO. |
|---|
| 71 | |
|---|
| 72 | leapdays: |
|---|
| 73 | Will add given days to the date found, if year is a leap |
|---|
| 74 | year, and the date found is post 28 of february. |
|---|
| 75 | |
|---|
| 76 | yearday, nlyearday: |
|---|
| 77 | Set the yearday or the non-leap year day (jump leap days). |
|---|
| 78 | These are converted to day/month/leapdays information. |
|---|
| 79 | |
|---|
| 80 | Here is the behavior of operations with relativedelta: |
|---|
| 81 | |
|---|
| 82 | 1) Calculate the absolute year, using the 'year' argument, or the |
|---|
| 83 | original datetime year, if the argument is not present. |
|---|
| 84 | |
|---|
| 85 | 2) Add the relative 'years' argument to the absolute year. |
|---|
| 86 | |
|---|
| 87 | 3) Do steps 1 and 2 for month/months. |
|---|
| 88 | |
|---|
| 89 | 4) Calculate the absolute day, using the 'day' argument, or the |
|---|
| 90 | original datetime day, if the argument is not present. Then, |
|---|
| 91 | subtract from the day until it fits in the year and month |
|---|
| 92 | found after their operations. |
|---|
| 93 | |
|---|
| 94 | 5) Add the relative 'days' argument to the absolute day. Notice |
|---|
| 95 | that the 'weeks' argument is multiplied by 7 and added to |
|---|
| 96 | 'days'. |
|---|
| 97 | |
|---|
| 98 | 6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, |
|---|
| 99 | microsecond/microseconds. |
|---|
| 100 | |
|---|
| 101 | 7) If the 'weekday' argument is present, calculate the weekday, |
|---|
| 102 | with the given (wday, nth) tuple. wday is the index of the |
|---|
| 103 | weekday (0-6, 0=Mon), and nth is the number of weeks to add |
|---|
| 104 | forward or backward, depending on its signal. Notice that if |
|---|
| 105 | the calculated date is already Monday, for example, using |
|---|
| 106 | (0, 1) or (0, -1) won't change the day. |
|---|
| 107 | """ |
|---|
| 108 | |
|---|
| 109 | def __init__(self, dt1=None, dt2=None, |
|---|
| 110 | years=0, months=0, days=0, leapdays=0, weeks=0, |
|---|
| 111 | hours=0, minutes=0, seconds=0, microseconds=0, |
|---|
| 112 | year=None, month=None, day=None, weekday=None, |
|---|
| 113 | yearday=None, nlyearday=None, |
|---|
| 114 | hour=None, minute=None, second=None, microsecond=None): |
|---|
| 115 | if dt1 and dt2: |
|---|
| 116 | if not isinstance(dt1, datetime.date) or \ |
|---|
| 117 | not isinstance(dt2, datetime.date): |
|---|
| 118 | raise TypeError, "relativedelta only diffs datetime/date" |
|---|
| 119 | if type(dt1) is not type(dt2): |
|---|
| 120 | if not isinstance(dt1, datetime.datetime): |
|---|
| 121 | dt1 = datetime.datetime.fromordinal(dt1.toordinal()) |
|---|
| 122 | elif not isinstance(dt2, datetime.datetime): |
|---|
| 123 | dt2 = datetime.datetime.fromordinal(dt2.toordinal()) |
|---|
| 124 | self.years = 0 |
|---|
| 125 | self.months = 0 |
|---|
| 126 | self.days = 0 |
|---|
| 127 | self.leapdays = 0 |
|---|
| 128 | self.hours = 0 |
|---|
| 129 | self.minutes = 0 |
|---|
| 130 | self.seconds = 0 |
|---|
| 131 | self.microseconds = 0 |
|---|
| 132 | self.year = None |
|---|
| 133 | self.month = None |
|---|
| 134 | self.day = None |
|---|
| 135 | self.weekday = None |
|---|
| 136 | self.hour = None |
|---|
| 137 | self.minute = None |
|---|
| 138 | self.second = None |
|---|
| 139 | self.microsecond = None |
|---|
| 140 | self._has_time = 0 |
|---|
| 141 | |
|---|
| 142 | months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month) |
|---|
| 143 | self._set_months(months) |
|---|
| 144 | dtm = self.__radd__(dt2) |
|---|
| 145 | if dt1 < dt2: |
|---|
| 146 | while dt1 > dtm: |
|---|
| 147 | months += 1 |
|---|
| 148 | self._set_months(months) |
|---|
| 149 | dtm = self.__radd__(dt2) |
|---|
| 150 | else: |
|---|
| 151 | while dt1 < dtm: |
|---|
| 152 | months -= 1 |
|---|
| 153 | self._set_months(months) |
|---|
| 154 | dtm = self.__radd__(dt2) |
|---|
| 155 | delta = dt1 - dtm |
|---|
| 156 | self.seconds = delta.seconds+delta.days*86400 |
|---|
| 157 | self.microseconds = delta.microseconds |
|---|
| 158 | else: |
|---|
| 159 | self.years = years |
|---|
| 160 | self.months = months |
|---|
| 161 | self.days = days+weeks*7 |
|---|
| 162 | self.leapdays = leapdays |
|---|
| 163 | self.hours = hours |
|---|
| 164 | self.minutes = minutes |
|---|
| 165 | self.seconds = seconds |
|---|
| 166 | self.microseconds = microseconds |
|---|
| 167 | self.year = year |
|---|
| 168 | self.month = month |
|---|
| 169 | self.day = day |
|---|
| 170 | self.hour = hour |
|---|
| 171 | self.minute = minute |
|---|
| 172 | self.second = second |
|---|
| 173 | self.microsecond = microsecond |
|---|
| 174 | |
|---|
| 175 | if type(weekday) is int: |
|---|
| 176 | self.weekday = weekdays[weekday] |
|---|
| 177 | else: |
|---|
| 178 | self.weekday = weekday |
|---|
| 179 | |
|---|
| 180 | yday = 0 |
|---|
| 181 | if nlyearday: |
|---|
| 182 | yday = nlyearday |
|---|
| 183 | elif yearday: |
|---|
| 184 | yday = yearday |
|---|
| 185 | if yearday > 59: |
|---|
| 186 | self.leapdays = -1 |
|---|
| 187 | if yday: |
|---|
| 188 | ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366] |
|---|
| 189 | for idx, ydays in enumerate(ydayidx): |
|---|
| 190 | if yday <= ydays: |
|---|
| 191 | self.month = idx+1 |
|---|
| 192 | if idx == 0: |
|---|
| 193 | self.day = ydays |
|---|
| 194 | else: |
|---|
| 195 | self.day = yday-ydayidx[idx-1] |
|---|
| 196 | break |
|---|
| 197 | else: |
|---|
| 198 | raise ValueError, "invalid year day (%d)" % yday |
|---|
| 199 | |
|---|
| 200 | self._fix() |
|---|
| 201 | |
|---|
| 202 | def _fix(self): |
|---|
| 203 | if abs(self.microseconds) > 999999: |
|---|
| 204 | s = self.microseconds/abs(self.microseconds) |
|---|
| 205 | div, mod = divmod(self.microseconds*s, 1000000) |
|---|
| 206 | self.microseconds = mod*s |
|---|
| 207 | self.seconds += div*s |
|---|
| 208 | if abs(self.seconds) > 59: |
|---|
| 209 | s = self.seconds/abs(self.seconds) |
|---|
| 210 | div, mod = divmod(self.seconds*s, 60) |
|---|
| 211 | self.seconds = mod*s |
|---|
| 212 | self.minutes += div*s |
|---|
| 213 | if abs(self.minutes) > 59: |
|---|
| 214 | s = self.minutes/abs(self.minutes) |
|---|
| 215 | div, mod = divmod(self.minutes*s, 60) |
|---|
| 216 | self.minutes = mod*s |
|---|
| 217 | self.hours += div*s |
|---|
| 218 | if abs(self.hours) > 23: |
|---|
| 219 | s = self.hours/abs(self.hours) |
|---|
| 220 | div, mod = divmod(self.hours*s, 24) |
|---|
| 221 | self.hours = mod*s |
|---|
| 222 | self.days += div*s |
|---|
| 223 | if abs(self.months) > 11: |
|---|
| 224 | s = self.months/abs(self.months) |
|---|
| 225 | div, mod = divmod(self.months*s, 12) |
|---|
| 226 | self.months = mod*s |
|---|
| 227 | self.years += div*s |
|---|
| 228 | if (self.hours or self.minutes or self.seconds or self.microseconds or |
|---|
| 229 | self.hour is not None or self.minute is not None or |
|---|
| 230 | self.second is not None or self.microsecond is not None): |
|---|
| 231 | self._has_time = 1 |
|---|
| 232 | else: |
|---|
| 233 | self._has_time = 0 |
|---|
| 234 | |
|---|
| 235 | def _set_months(self, months): |
|---|
| 236 | self.months = months |
|---|
| 237 | if abs(self.months) > 11: |
|---|
| 238 | s = self.months/abs(self.months) |
|---|
| 239 | div, mod = divmod(self.months*s, 12) |
|---|
| 240 | self.months = mod*s |
|---|
| 241 | self.years = div*s |
|---|
| 242 | else: |
|---|
| 243 | self.years = 0 |
|---|
| 244 | |
|---|
| 245 | def __radd__(self, other): |
|---|
| 246 | if not isinstance(other, datetime.date): |
|---|
| 247 | raise TypeError, "unsupported type for add operation" |
|---|
| 248 | elif self._has_time and not isinstance(other, datetime.datetime): |
|---|
| 249 | other = datetime.datetime.fromordinal(other.toordinal()) |
|---|
| 250 | year = (self.year or other.year)+self.years |
|---|
| 251 | month = self.month or other.month |
|---|
| 252 | if self.months: |
|---|
| 253 | assert 1 <= abs(self.months) <= 12 |
|---|
| 254 | month += self.months |
|---|
| 255 | if month > 12: |
|---|
| 256 | year += 1 |
|---|
| 257 | month -= 12 |
|---|
| 258 | elif month < 1: |
|---|
| 259 | year -= 1 |
|---|
| 260 | month += 12 |
|---|
| 261 | day = min(calendar.monthrange(year, month)[1], |
|---|
| 262 | self.day or other.day) |
|---|
| 263 | repl = {"year": year, "month": month, "day": day} |
|---|
| 264 | for attr in ["hour", "minute", "second", "microsecond"]: |
|---|
| 265 | value = getattr(self, attr) |
|---|
| 266 | if value is not None: |
|---|
| 267 | repl[attr] = value |
|---|
| 268 | days = self.days |
|---|
| 269 | if self.leapdays and month > 2 and calendar.isleap(year): |
|---|
| 270 | days += self.leapdays |
|---|
| 271 | ret = (other.replace(**repl) |
|---|
| 272 | + datetime.timedelta(days=days, |
|---|
| 273 | hours=self.hours, |
|---|
| 274 | minutes=self.minutes, |
|---|
| 275 | seconds=self.seconds, |
|---|
| 276 | microseconds=self.microseconds)) |
|---|
| 277 | if self.weekday: |
|---|
| 278 | weekday, nth = self.weekday.weekday, self.weekday.n or 1 |
|---|
| 279 | jumpdays = (abs(nth)-1)*7 |
|---|
| 280 | if nth > 0: |
|---|
| 281 | jumpdays += (7-ret.weekday()+weekday)%7 |
|---|
| 282 | else: |
|---|
| 283 | jumpdays += (ret.weekday()-weekday)%7 |
|---|
| 284 | jumpdays *= -1 |
|---|
| 285 | ret += datetime.timedelta(days=jumpdays) |
|---|
| 286 | return ret |
|---|
| 287 | |
|---|
| 288 | def __rsub__(self, other): |
|---|
| 289 | return self.__neg__().__radd__(other) |
|---|
| 290 | |
|---|
| 291 | def __add__(self, other): |
|---|
| 292 | if not isinstance(other, relativedelta): |
|---|
| 293 | raise TypeError, "unsupported type for add operation" |
|---|
| 294 | return relativedelta(years=other.years+self.years, |
|---|
| 295 | months=other.months+self.months, |
|---|
| 296 | days=other.days+self.days, |
|---|
| 297 | hours=other.hours+self.hours, |
|---|
| 298 | minutes=other.minutes+self.minutes, |
|---|
| 299 | seconds=other.seconds+self.seconds, |
|---|
| 300 | microseconds=other.microseconds+self.microseconds, |
|---|
| 301 | leapdays=other.leapdays or self.leapdays, |
|---|
| 302 | year=other.year or self.year, |
|---|
| 303 | month=other.month or self.month, |
|---|
| 304 | day=other.day or self.day, |
|---|
| 305 | weekday=other.weekday or self.weekday, |
|---|
| 306 | hour=other.hour or self.hour, |
|---|
| 307 | minute=other.minute or self.minute, |
|---|
| 308 | second=other.second or self.second, |
|---|
| 309 | microsecond=other.second or self.microsecond) |
|---|
| 310 | |
|---|
| 311 | def __sub__(self, other): |
|---|
| 312 | if not isinstance(other, relativedelta): |
|---|
| 313 | raise TypeError, "unsupported type for sub operation" |
|---|
| 314 | return relativedelta(years=other.years-self.years, |
|---|
| 315 | months=other.months-self.months, |
|---|
| 316 | days=other.days-self.days, |
|---|
| 317 | hours=other.hours-self.hours, |
|---|
| 318 | minutes=other.minutes-self.minutes, |
|---|
| 319 | seconds=other.seconds-self.seconds, |
|---|
| 320 | microseconds=other.microseconds-self.microseconds, |
|---|
| 321 | leapdays=other.leapdays or self.leapdays, |
|---|
| 322 | year=other.year or self.year, |
|---|
| 323 | month=other.month or self.month, |
|---|
| 324 | day=other.day or self.day, |
|---|
| 325 | weekday=other.weekday or self.weekday, |
|---|
| 326 | hour=other.hour or self.hour, |
|---|
| 327 | minute=other.minute or self.minute, |
|---|
| 328 | second=other.second or self.second, |
|---|
| 329 | microsecond=other.second or self.microsecond) |
|---|
| 330 | |
|---|
| 331 | def __neg__(self): |
|---|
| 332 | return relativedelta(years=-self.years, |
|---|
| 333 | months=-self.months, |
|---|
| 334 | days=-self.days, |
|---|
| 335 | hours=-self.hours, |
|---|
| 336 | minutes=-self.minutes, |
|---|
| 337 | seconds=-self.seconds, |
|---|
| 338 | microseconds=-self.microseconds, |
|---|
| 339 | leapdays=self.leapdays, |
|---|
| 340 | year=self.year, |
|---|
| 341 | month=self.month, |
|---|
| 342 | day=self.day, |
|---|
| 343 | weekday=self.weekday, |
|---|
| 344 | hour=self.hour, |
|---|
| 345 | minute=self.minute, |
|---|
| 346 | second=self.second, |
|---|
| 347 | microsecond=self.microsecond) |
|---|
| 348 | |
|---|
| 349 | def __nonzero__(self): |
|---|
| 350 | return not (not self.years and |
|---|
| 351 | not self.months and |
|---|
| 352 | not self.days and |
|---|
| 353 | not self.hours and |
|---|
| 354 | not self.minutes and |
|---|
| 355 | not self.seconds and |
|---|
| 356 | not self.microseconds and |
|---|
| 357 | not self.leapdays and |
|---|
| 358 | self.year is None and |
|---|
| 359 | self.month is None and |
|---|
| 360 | self.day is None and |
|---|
| 361 | self.weekday is None and |
|---|
| 362 | self.hour is None and |
|---|
| 363 | self.minute is None and |
|---|
| 364 | self.second is None and |
|---|
| 365 | self.microsecond is None) |
|---|
| 366 | |
|---|
| 367 | def __mul__(self, other): |
|---|
| 368 | f = float(other) |
|---|
| 369 | return relativedelta(years=self.years*f, |
|---|
| 370 | months=self.months*f, |
|---|
| 371 | days=self.days*f, |
|---|
| 372 | hours=self.hours*f, |
|---|
| 373 | minutes=self.minutes*f, |
|---|
| 374 | seconds=self.seconds*f, |
|---|
| 375 | microseconds=self.microseconds*f, |
|---|
| 376 | leapdays=self.leapdays, |
|---|
| 377 | year=self.year, |
|---|
| 378 | month=self.month, |
|---|
| 379 | day=self.day, |
|---|
| 380 | weekday=self.weekday, |
|---|
| 381 | hour=self.hour, |
|---|
| 382 | minute=self.minute, |
|---|
| 383 | second=self.second, |
|---|
| 384 | microsecond=self.microsecond) |
|---|
| 385 | |
|---|
| 386 | def __eq__(self, other): |
|---|
| 387 | if not isinstance(other, relativedelta): |
|---|
| 388 | return False |
|---|
| 389 | if self.weekday or other.weekday: |
|---|
| 390 | if not self.weekday or not other.weekday: |
|---|
| 391 | return False |
|---|
| 392 | if self.weekday.weekday != other.weekday.weekday: |
|---|
| 393 | return False |
|---|
| 394 | n1, n2 = self.weekday.n, other.weekday.n |
|---|
| 395 | if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): |
|---|
| 396 | return False |
|---|
| 397 | return (self.years == other.years and |
|---|
| 398 | self.months == other.months and |
|---|
| 399 | self.days == other.days and |
|---|
| 400 | self.hours == other.hours and |
|---|
| 401 | self.minutes == other.minutes and |
|---|
| 402 | self.seconds == other.seconds and |
|---|
| 403 | self.leapdays == other.leapdays and |
|---|
| 404 | self.year == other.year and |
|---|
| 405 | self.month == other.month and |
|---|
| 406 | self.day == other.day and |
|---|
| 407 | self.hour == other.hour and |
|---|
| 408 | self.minute == other.minute and |
|---|
| 409 | self.second == other.second and |
|---|
| 410 | self.microsecond == other.microsecond) |
|---|
| 411 | |
|---|
| 412 | def __ne__(self, other): |
|---|
| 413 | return not self.__eq__(other) |
|---|
| 414 | |
|---|
| 415 | def __div__(self, other): |
|---|
| 416 | return self.__mul__(1/float(other)) |
|---|
| 417 | |
|---|
| 418 | def __repr__(self): |
|---|
| 419 | l = [] |
|---|
| 420 | for attr in ["years", "months", "days", "leapdays", |
|---|
| 421 | "hours", "minutes", "seconds", "microseconds"]: |
|---|
| 422 | value = getattr(self, attr) |
|---|
| 423 | if value: |
|---|
| 424 | l.append("%s=%+d" % (attr, value)) |
|---|
| 425 | for attr in ["year", "month", "day", "weekday", |
|---|
| 426 | "hour", "minute", "second", "microsecond"]: |
|---|
| 427 | value = getattr(self, attr) |
|---|
| 428 | if value is not None: |
|---|
| 429 | l.append("%s=%s" % (attr, `value`)) |
|---|
| 430 | return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) |
|---|
| 431 | |
|---|
| 432 | # vim:ts=4:sw=4:et |
|---|