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