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