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