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