1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - DateSupport library (derived from EventAggregatorSupport) 4 5 @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 import calendar 10 import datetime 11 import re 12 import bisect 13 14 try: 15 import pytz 16 except ImportError: 17 pytz = None 18 19 # Date labels. 20 21 month_labels = ["January", "February", "March", "April", "May", "June", 22 "July", "August", "September", "October", "November", "December"] 23 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 24 25 # Month, date, time and datetime parsing. 26 27 month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})' 28 date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})' 29 time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?' 30 timezone_offset_str = ur'(?P<offset>(UTC)?(?:(?P<sign>[-+])?(?P<hours>[0-9]{2})(?::?(?P<minutes>[0-9]{2}))?))' 31 timezone_olson_str = ur'(?P<olson>[a-zA-Z]+(?:/[-_a-zA-Z]+){1,2})' 32 timezone_utc_str = ur'UTC' 33 timezone_regexp_str = ur'(?P<zone>' + timezone_offset_str + '|' + timezone_olson_str + '|' + timezone_utc_str + ')' 34 datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?' 35 36 month_regexp = re.compile(month_regexp_str, re.UNICODE) 37 date_regexp = re.compile(date_regexp_str, re.UNICODE) 38 time_regexp = re.compile(time_regexp_str, re.UNICODE) 39 timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE) 40 timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE) 41 datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE) 42 43 # iCalendar date and datetime parsing. 44 45 date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})' 46 datetime_icalendar_regexp_str = date_icalendar_regexp_str + \ 47 ur'(?:' \ 48 ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \ 49 ur'(?P<utc>Z)?' \ 50 ur')?' 51 52 date_icalendar_regexp = re.compile(date_icalendar_regexp_str, re.UNICODE) 53 datetime_icalendar_regexp = re.compile(datetime_icalendar_regexp_str, re.UNICODE) 54 55 # Utility functions. 56 57 def sign(x): 58 if x < 0: 59 return -1 60 else: 61 return 1 62 63 def int_or_none(x): 64 if x is None: 65 return x 66 else: 67 return int(x) 68 69 def getMonthLabel(month): 70 71 "Return an unlocalised label for the given 'month'." 72 73 return month_labels[month - 1] # zero-based labels 74 75 def getDayLabel(weekday): 76 77 "Return an unlocalised label for the given 'weekday'." 78 79 return weekday_labels[weekday] 80 81 # Interfaces. 82 83 class ActsAsTimespan: 84 pass 85 86 # Date-related functions. 87 88 def cmp_dates_as_day_start(a, b): 89 90 """ 91 Compare dates/datetimes 'a' and 'b' treating dates without time information 92 as the earliest time in a particular day. 93 """ 94 95 are_equal = a == b 96 97 if are_equal: 98 a2 = a.as_datetime_or_date() 99 b2 = b.as_datetime_or_date() 100 101 if isinstance(a2, Date) and isinstance(b2, DateTime): 102 return -1 103 elif isinstance(a2, DateTime) and isinstance(b2, Date): 104 return 1 105 106 return cmp(a, b) 107 108 class Convertible: 109 110 "Support for converting temporal objects." 111 112 def _get_converter(self, resolution): 113 if resolution == "month": 114 return lambda x: x and x.as_month() 115 elif resolution == "date": 116 return lambda x: x and x.as_date() 117 elif resolution == "datetime": 118 return lambda x: x and x.as_datetime_or_date() 119 else: 120 return lambda x: x 121 122 class Temporal(Convertible): 123 124 "A simple temporal representation, common to dates and times." 125 126 def __init__(self, data): 127 self.data = list(data) 128 129 def __repr__(self): 130 return "%s(%r)" % (self.__class__.__name__, self.data) 131 132 def __hash__(self): 133 return hash(self.as_tuple()) 134 135 def as_tuple(self): 136 return tuple(self.data) 137 138 def convert(self, resolution): 139 return self._get_converter(resolution)(self) 140 141 def __cmp__(self, other): 142 143 """ 144 The result of comparing this instance with 'other' is derived from a 145 comparison of the instances' date(time) data at the highest common 146 resolution, meaning that if a date is compared to a datetime, the 147 datetime will be considered as a date. Thus, a date and a datetime 148 referring to the same date will be considered equal. 149 """ 150 151 if not isinstance(other, Temporal): 152 return NotImplemented 153 else: 154 data = self.as_tuple() 155 other_data = other.as_tuple() 156 length = min(len(data), len(other_data)) 157 return cmp(data[:length], other_data[:length]) 158 159 def __sub__(self, other): 160 161 """ 162 Return the difference between this object and the 'other' object at the 163 highest common accuracy of both objects. 164 """ 165 166 if not isinstance(other, Temporal): 167 return NotImplemented 168 else: 169 data = self.as_tuple() 170 other_data = other.as_tuple() 171 direction = self < other and 1 or -1 172 173 if len(data) < len(other_data): 174 return (len(self.until(other)) - 1) * direction 175 else: 176 return (len(other.until(self)) - 1) * -direction 177 178 def _until(self, start, end, nextfn, prevfn): 179 180 """ 181 Return a collection of units of time by starting from the given 'start' 182 and stepping across intervening units until 'end' is reached, using the 183 given 'nextfn' and 'prevfn' to step from one unit to the next. 184 """ 185 186 current = start 187 units = [current] 188 if current < end: 189 while current < end: 190 current = nextfn(current) 191 units.append(current) 192 elif current > end: 193 while current > end: 194 current = prevfn(current) 195 units.append(current) 196 return units 197 198 def ambiguous(self): 199 200 "Only times can be ambiguous." 201 202 return 0 203 204 class Month(Temporal): 205 206 "A simple year-month representation." 207 208 def __str__(self): 209 return "%04d-%02d" % self.as_tuple()[:2] 210 211 def as_datetime(self, day, hour, minute, second, zone): 212 return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) 213 214 def as_date(self, day): 215 if day < 0: 216 weekday, ndays = self.month_properties() 217 day = ndays + 1 + day 218 return Date(self.as_tuple() + (day,)) 219 220 def as_month(self): 221 return self 222 223 def year(self): 224 return self.data[0] 225 226 def month(self): 227 return self.data[1] 228 229 def month_properties(self): 230 231 """ 232 Return the weekday of the 1st of the month, along with the number of 233 days, as a tuple. 234 """ 235 236 year, month = self.as_tuple()[:2] 237 return calendar.monthrange(year, month) 238 239 def month_update(self, n=1): 240 241 "Return the month updated by 'n' months." 242 243 year, month = self.as_tuple()[:2] 244 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1)) 245 246 update = month_update 247 248 def next_month(self): 249 250 "Return the month following this one." 251 252 return self.month_update(1) 253 254 next = next_month 255 256 def previous_month(self): 257 258 "Return the month preceding this one." 259 260 return self.month_update(-1) 261 262 previous = previous_month 263 264 def months_until(self, end): 265 266 "Return the collection of months from this month until 'end'." 267 268 return self._until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month) 269 270 until = months_until 271 272 class Date(Month): 273 274 "A simple year-month-day representation." 275 276 def constrain(self): 277 year, month, day = self.as_tuple()[:3] 278 279 month = max(min(month, 12), 1) 280 wd, last_day = calendar.monthrange(year, month) 281 day = max(min(day, last_day), 1) 282 283 self.data[1:3] = month, day 284 285 def __str__(self): 286 return "%04d-%02d-%02d" % self.as_tuple()[:3] 287 288 def as_datetime(self, hour, minute, second, zone): 289 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 290 291 def as_start_of_day(self): 292 return self.as_datetime(None, None, None, None) 293 294 def as_date(self): 295 return self 296 297 def as_datetime_or_date(self): 298 return self 299 300 def as_month(self): 301 return Month(self.data[:2]) 302 303 def day(self): 304 return self.data[2] 305 306 def day_update(self, n=1): 307 308 "Return the month updated by 'n' days." 309 310 delta = datetime.timedelta(n) 311 dt = datetime.date(*self.as_tuple()[:3]) 312 dt_new = dt + delta 313 return Date((dt_new.year, dt_new.month, dt_new.day)) 314 315 update = day_update 316 317 def next_day(self): 318 319 "Return the date following this one." 320 321 year, month, day = self.as_tuple()[:3] 322 _wd, end_day = calendar.monthrange(year, month) 323 if day == end_day: 324 if month == 12: 325 return Date((year + 1, 1, 1)) 326 else: 327 return Date((year, month + 1, 1)) 328 else: 329 return Date((year, month, day + 1)) 330 331 next = next_day 332 333 def previous_day(self): 334 335 "Return the date preceding this one." 336 337 year, month, day = self.as_tuple()[:3] 338 if day == 1: 339 if month == 1: 340 return Date((year - 1, 12, 31)) 341 else: 342 _wd, end_day = calendar.monthrange(year, month - 1) 343 return Date((year, month - 1, end_day)) 344 else: 345 return Date((year, month, day - 1)) 346 347 previous = previous_day 348 349 def days_until(self, end): 350 351 "Return the collection of days from this date until 'end'." 352 353 return self._until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 354 355 until = days_until 356 357 class DateTime(Date): 358 359 "A simple date plus time representation." 360 361 def constrain(self): 362 Date.constrain(self) 363 364 hour, minute, second = self.as_tuple()[3:6] 365 366 if self.has_time(): 367 hour = max(min(hour, 23), 0) 368 minute = max(min(minute, 59), 0) 369 370 if second is not None: 371 second = max(min(second, 60), 0) # support leap seconds 372 373 self.data[3:6] = hour, minute, second 374 375 def __str__(self): 376 return Date.__str__(self) + self.time_string() 377 378 def time_string(self, zone_as_offset=0): 379 if self.has_time(): 380 data = self.as_tuple() 381 time_str = " %02d:%02d" % data[3:5] 382 if data[5] is not None: 383 time_str += ":%02d" % data[5] 384 if data[6] is not None: 385 if zone_as_offset: 386 utc_offset = self.utc_offset() 387 if utc_offset: 388 time_str += " %+03d:%02d" % utc_offset 389 else: 390 time_str += " %s" % data[6] 391 return time_str 392 else: 393 return "" 394 395 def as_HTTP_datetime_string(self): 396 weekday = calendar.weekday(*self.data[:3]) 397 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (( 398 getDayLabel(weekday), 399 self.data[2], 400 getMonthLabel(self.data[1]), 401 self.data[0] 402 ) + tuple(self.data[3:6])) 403 404 def as_ISO8601_datetime_string(self): 405 return Date.__str__(self) + self.time_string(1) 406 407 def as_datetime(self): 408 return self 409 410 def as_date(self): 411 return Date(self.data[:3]) 412 413 def as_datetime_or_date(self): 414 415 """ 416 Return a date for this datetime if fields are missing. Otherwise, return 417 this datetime itself. 418 """ 419 420 if not self.has_time(): 421 return self.as_date() 422 else: 423 return self 424 425 def __cmp__(self, other): 426 427 """ 428 The result of comparing this instance with 'other' is, if both instances 429 are datetime instances, derived from a comparison of the datetimes 430 converted to UTC. If one or both datetimes cannot be converted to UTC, 431 the datetimes are compared using the basic temporal comparison which 432 compares their raw time data. 433 """ 434 435 this = self 436 437 if this.has_time(): 438 if isinstance(other, DateTime): 439 if other.has_time(): 440 this_utc = this.to_utc() 441 other_utc = other.to_utc() 442 if this_utc is not None and other_utc is not None: 443 return cmp(this_utc.as_tuple(), other_utc.as_tuple()) 444 else: 445 other = other.padded() 446 else: 447 this = this.padded() 448 449 return Date.__cmp__(this, other) 450 451 def __sub__(self, other): 452 453 """ 454 Return the difference between this object and the 'other' object at the 455 highest common accuracy of both objects. 456 """ 457 458 if not isinstance(other, Temporal): 459 return NotImplemented 460 elif not other.has_time(): 461 return self.as_date() - other 462 else: 463 utc = self.to_utc() 464 other = other.to_utc() 465 days = utc.as_date() - other.as_date() 466 h1, m1, s1 = utc.as_tuple()[3:6] 467 h2, m2, s2 = other.as_tuple()[3:6] 468 return days * 24 * 3600 + (h1 - h2) * 3600 + (m1 - m2) * 60 + s1 - s2 469 470 def has_time(self): 471 472 """ 473 Return whether this object has any time information. Objects without 474 time information can refer to the very start of a day. 475 """ 476 477 return self.data[3] is not None and self.data[4] is not None 478 479 def time(self): 480 return self.data[3:] 481 482 def seconds(self): 483 return self.data[5] 484 485 def time_zone(self): 486 return self.data[6] 487 488 def set_time_zone(self, value): 489 self.data[6] = value 490 491 def padded(self, empty_value=0): 492 493 """ 494 Return a datetime with missing fields defined as being the given 495 'empty_value' or 0 if not specified. 496 """ 497 498 data = [] 499 for x in self.data[:6]: 500 if x is None: 501 data.append(empty_value) 502 else: 503 data.append(x) 504 505 data += self.data[6:] 506 return DateTime(data) 507 508 def to_utc(self): 509 510 """ 511 Return this object converted to UTC, or None if such a conversion is not 512 defined. 513 """ 514 515 if not self.has_time(): 516 return None 517 518 offset = self.utc_offset() 519 if offset: 520 hours, minutes = offset 521 522 # Invert the offset to get the correction. 523 524 hours, minutes = -hours, -minutes 525 526 # Get the components. 527 528 hour, minute, second, zone = self.time() 529 date = self.as_date() 530 531 # Add the minutes and hours. 532 533 minute += minutes 534 if minute < 0 or minute > 59: 535 hour += minute / 60 536 minute = minute % 60 537 538 # NOTE: This makes various assumptions and probably would not work 539 # NOTE: for general arithmetic. 540 541 hour += hours 542 if hour < 0: 543 date = date.previous_day() 544 hour += 24 545 elif hour > 23: 546 date = date.next_day() 547 hour -= 24 548 549 return date.as_datetime(hour, minute, second, "UTC") 550 551 # Cannot convert. 552 553 else: 554 return None 555 556 def utc_offset(self): 557 558 "Return the UTC offset in hours and minutes." 559 560 zone = self.time_zone() 561 if not zone: 562 return None 563 564 # Support explicit UTC zones. 565 566 if zone == "UTC": 567 return 0, 0 568 569 # Attempt to return a UTC offset where an explicit offset has been set. 570 571 match = timezone_offset_regexp.match(zone) 572 if match: 573 if match.group("sign") == "-": 574 offset_sign = -1 575 else: 576 offset_sign = 1 577 578 hours = int(match.group("hours")) * offset_sign 579 minutes = int(match.group("minutes") or 0) * offset_sign 580 return hours, minutes 581 582 # Attempt to handle Olson time zone identifiers. 583 584 dt = self.as_olson_datetime() 585 if dt: 586 seconds = dt.utcoffset().seconds + dt.utcoffset().days * 24 * 3600 587 hours = abs(seconds) / 3600 588 minutes = (abs(seconds) % 3600) / 60 589 return sign(seconds) * hours, sign(seconds) * minutes 590 591 # Otherwise return None. 592 593 return None 594 595 def olson_identifier(self): 596 597 "Return the Olson identifier from any zone information." 598 599 zone = self.time_zone() 600 if not zone: 601 return None 602 603 # Attempt to match an identifier. 604 605 match = timezone_olson_regexp.match(zone) 606 if match: 607 return match.group("olson") 608 else: 609 return None 610 611 def _as_olson_datetime(self, hours=None): 612 613 """ 614 Return a Python datetime object for this datetime interpreted using any 615 Olson time zone identifier and the given 'hours' offset, raising one of 616 the pytz exceptions in case of ambiguity. 617 """ 618 619 olson = self.olson_identifier() 620 if olson and pytz: 621 tz = pytz.timezone(olson) 622 data = self.padded().as_tuple()[:6] 623 dt = datetime.datetime(*data) 624 625 # With an hours offset, find a time probably in a previously 626 # applicable time zone. 627 628 if hours is not None: 629 td = datetime.timedelta(0, hours * 3600) 630 dt += td 631 632 ldt = tz.localize(dt, None) 633 634 # With an hours offset, adjust the time to define it within the 635 # previously applicable time zone but at the presumably intended 636 # position. 637 638 if hours is not None: 639 ldt -= td 640 641 return ldt 642 else: 643 return None 644 645 def as_olson_datetime(self): 646 647 """ 648 Return a Python datetime object for this datetime interpreted using any 649 Olson time zone identifier, choosing the time from the zone before the 650 period of ambiguity. 651 """ 652 653 try: 654 return self._as_olson_datetime() 655 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 656 657 # Try again, using an earlier local time and then stepping forward 658 # in the chosen zone. 659 # NOTE: Four hours earlier seems reasonable. 660 661 return self._as_olson_datetime(-4) 662 663 def ambiguous(self): 664 665 "Return whether the time is local and ambiguous." 666 667 try: 668 self._as_olson_datetime() 669 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 670 return 1 671 672 return 0 673 674 class Timespan(ActsAsTimespan, Convertible): 675 676 """ 677 A period of time which can be compared against others to check for overlaps. 678 """ 679 680 def __init__(self, start, end): 681 self.start = start 682 self.end = end 683 684 # NOTE: Should perhaps catch ambiguous time problems elsewhere. 685 686 if self.ambiguous() and self.start is not None and self.end is not None and start > end: 687 self.start, self.end = end, start 688 689 def __repr__(self): 690 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 691 692 def __hash__(self): 693 return hash((self.start, self.end)) 694 695 def as_timespan(self): 696 return self 697 698 def as_limits(self): 699 return self.start, self.end 700 701 def ambiguous(self): 702 return self.start is not None and self.start.ambiguous() or self.end is not None and self.end.ambiguous() 703 704 def convert(self, resolution): 705 return Timespan(*map(self._get_converter(resolution), self.as_limits())) 706 707 def is_before(self, a, b): 708 709 """ 710 Return whether 'a' is before 'b'. Since the end datetime of one period 711 may be the same as the start datetime of another period, and yet the 712 first period is intended to be concluded by the end datetime and not 713 overlap with the other period, a different test is employed for datetime 714 comparisons. 715 """ 716 717 # Datetimes without times can be equal to dates and be considered as 718 # occurring before those dates. Generally, datetimes should not be 719 # produced without time information as getDateTime converts such 720 # datetimes to dates. 721 722 if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()): 723 return a <= b 724 else: 725 return a < b 726 727 def __contains__(self, other): 728 729 """ 730 This instance is considered to contain 'other' if one is not before or 731 after the other. If this instance overlaps or coincides with 'other', 732 then 'other' is regarded as belonging to this instance's time period. 733 """ 734 735 return self == other 736 737 def __cmp__(self, other): 738 739 """ 740 Return whether this timespan occupies the same period of time as the 741 'other'. Timespans are considered less than others if their end points 742 precede the other's start point, and are considered greater than others 743 if their start points follow the other's end point. 744 """ 745 746 if isinstance(other, ActsAsTimespan): 747 other = other.as_timespan() 748 749 if self.end is not None and other.start is not None and self.is_before(self.end, other.start): 750 return -1 751 elif self.start is not None and other.end is not None and self.is_before(other.end, self.start): 752 return 1 753 else: 754 return 0 755 756 else: 757 if self.end is not None and self.is_before(self.end, other): 758 return -1 759 elif self.start is not None and self.is_before(other, self.start): 760 return 1 761 else: 762 return 0 763 764 class TimespanCollection: 765 766 """ 767 A class providing a list-like interface supporting membership tests at a 768 particular resolution in order to maintain a collection of non-overlapping 769 timespans. 770 """ 771 772 def __init__(self, resolution, values=None): 773 self.resolution = resolution 774 self.values = values or [] 775 776 def as_timespan(self): 777 return Timespan(*self.as_limits()) 778 779 def as_limits(self): 780 781 "Return the earliest and latest points in time for this collection." 782 783 if not self.values: 784 return None, None 785 else: 786 first, last = self.values[0], self.values[-1] 787 if isinstance(first, ActsAsTimespan): 788 first = first.as_timespan().start 789 if isinstance(last, ActsAsTimespan): 790 last = last.as_timespan().end 791 return first, last 792 793 def convert(self, value): 794 if isinstance(value, ActsAsTimespan): 795 ts = value.as_timespan() 796 return ts and ts.convert(self.resolution) 797 else: 798 return value.convert(self.resolution) 799 800 def __iter__(self): 801 return iter(self.values) 802 803 def __len__(self): 804 return len(self.values) 805 806 def __getitem__(self, i): 807 return self.values[i] 808 809 def __setitem__(self, i, value): 810 self.values[i] = value 811 812 def __contains__(self, value): 813 test_value = self.convert(value) 814 return test_value in self.values 815 816 def append(self, value): 817 self.values.append(value) 818 819 def insert(self, i, value): 820 self.values.insert(i, value) 821 822 def pop(self): 823 return self.values.pop() 824 825 def insert_in_order(self, value): 826 bisect.insort_left(self, value) 827 828 def getDate(s): 829 830 "Parse the string 's', extracting and returning a date object." 831 832 dt = getDateTime(s) 833 if dt is not None: 834 return dt.as_date() 835 else: 836 return None 837 838 def getDateTime(s): 839 840 """ 841 Parse the string 's', extracting and returning a datetime object where time 842 information has been given or a date object where time information is 843 absent. 844 """ 845 846 m = datetime_regexp.search(s) 847 if m: 848 groups = list(m.groups()) 849 850 # Convert date and time data to integer or None. 851 852 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date() 853 else: 854 return None 855 856 def getDateFromCalendar(s): 857 858 """ 859 Parse the iCalendar format string 's', extracting and returning a date 860 object. 861 """ 862 863 dt = getDateTimeFromCalendar(s) 864 if dt is not None: 865 return dt.as_date() 866 else: 867 return None 868 869 def getDateTimeFromCalendar(s): 870 871 """ 872 Parse the iCalendar format datetime string 's', extracting and returning a 873 datetime object where time information has been given or a date object where 874 time information is absent. 875 """ 876 877 m = datetime_icalendar_regexp.search(s) 878 if m: 879 groups = list(m.groups()) 880 881 # Convert date and time data to integer or None. 882 883 return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or None]).as_datetime_or_date() 884 else: 885 return None 886 887 def getDateStrings(s): 888 889 "Parse the string 's', extracting and returning all date strings." 890 891 start = 0 892 m = date_regexp.search(s, start) 893 l = [] 894 while m: 895 l.append("-".join(m.groups())) 896 m = date_regexp.search(s, m.end()) 897 return l 898 899 def getMonth(s): 900 901 "Parse the string 's', extracting and returning a month object." 902 903 m = month_regexp.search(s) 904 if m: 905 return Month(map(int, m.groups())) 906 else: 907 return None 908 909 def getCurrentDate(): 910 911 "Return the current date as a Date instance." 912 913 today = datetime.date.today() 914 return Date((today.year, today.month, today.day)) 915 916 def getCurrentMonth(): 917 918 "Return the current month as a Month instance." 919 920 today = datetime.date.today() 921 return Month((today.year, today.month)) 922 923 def getCurrentYear(): 924 925 "Return the current year." 926 927 today = datetime.date.today() 928 return today.year 929 930 def getCurrentTime(): 931 932 "Return the current time as a DateTime instance." 933 934 now = datetime.datetime.utcnow() 935 return DateTime((now.year, now.month, now.day, now.hour, now.minute, now.second, "UTC")) 936 937 # vim: tabstop=4 expandtab shiftwidth=4