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 and pytz: 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 try: 669 return self._as_olson_datetime() 670 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 671 672 # Try again, using an earlier local time and then stepping forward 673 # in the chosen zone. 674 # NOTE: Four hours earlier seems reasonable. 675 676 return self._as_olson_datetime(-4) 677 678 def ambiguous(self): 679 680 "Return whether the time is local and ambiguous." 681 682 try: 683 self._as_olson_datetime() 684 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 685 return 1 686 687 return 0 688 689 class Timespan(ActsAsTimespan, Convertible): 690 691 """ 692 A period of time which can be compared against others to check for overlaps. 693 """ 694 695 def __init__(self, start, end): 696 self.start = start 697 self.end = end 698 699 # NOTE: Should perhaps catch ambiguous time problems elsewhere. 700 701 if self.ambiguous() and self.start is not None and self.end is not None and start > end: 702 self.start, self.end = end, start 703 704 def __repr__(self): 705 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 706 707 def __hash__(self): 708 return hash((self.start, self.end)) 709 710 def as_timespan(self): 711 return self 712 713 def as_limits(self): 714 return self.start, self.end 715 716 def ambiguous(self): 717 return self.start is not None and self.start.ambiguous() or self.end is not None and self.end.ambiguous() 718 719 def convert(self, resolution): 720 return Timespan(*map(self._get_converter(resolution), self.as_limits())) 721 722 def is_before(self, a, b): 723 724 """ 725 Return whether 'a' is before 'b'. Since the end datetime of one period 726 may be the same as the start datetime of another period, and yet the 727 first period is intended to be concluded by the end datetime and not 728 overlap with the other period, a different test is employed for datetime 729 comparisons. 730 """ 731 732 # Datetimes without times can be equal to dates and be considered as 733 # occurring before those dates. Generally, datetimes should not be 734 # produced without time information as getDateTime converts such 735 # datetimes to dates. 736 737 if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()): 738 return a <= b 739 else: 740 return a < b 741 742 def __contains__(self, other): 743 744 """ 745 This instance is considered to contain 'other' if one is not before or 746 after the other. If this instance overlaps or coincides with 'other', 747 then 'other' is regarded as belonging to this instance's time period. 748 """ 749 750 return self == other 751 752 def __cmp__(self, other): 753 754 """ 755 Return whether this timespan occupies the same period of time as the 756 'other'. Timespans are considered less than others if their end points 757 precede the other's start point, and are considered greater than others 758 if their start points follow the other's end point. 759 """ 760 761 if isinstance(other, ActsAsTimespan): 762 other = other.as_timespan() 763 764 before = self.end is not None and other.start is not None and self.is_before(self.end, other.start) 765 after = self.start is not None and other.end is not None and self.is_before(other.end, self.start) 766 else: 767 before = self.end is not None and self.is_before(self.end, other) 768 after = self.start is not None and self.is_before(other, self.start) 769 770 # Two identical points in time will be "before" each other according to 771 # the is_before test. 772 773 if not before and not after or before and after: 774 return 0 775 elif before: 776 return -1 777 else: 778 return 1 779 780 class TimespanCollection: 781 782 """ 783 A class providing a list-like interface supporting membership tests at a 784 particular resolution in order to maintain a collection of non-overlapping 785 timespans. 786 """ 787 788 def __init__(self, resolution, values=None): 789 self.resolution = resolution 790 self.values = values or [] 791 792 def as_timespan(self): 793 return Timespan(*self.as_limits()) 794 795 def as_limits(self): 796 797 "Return the earliest and latest points in time for this collection." 798 799 if not self.values: 800 return None, None 801 else: 802 first, last = self.values[0], self.values[-1] 803 if isinstance(first, ActsAsTimespan): 804 first = first.as_timespan().start 805 if isinstance(last, ActsAsTimespan): 806 last = last.as_timespan().end 807 return first, last 808 809 def convert(self, value): 810 if isinstance(value, ActsAsTimespan): 811 ts = value.as_timespan() 812 return ts and ts.convert(self.resolution) 813 else: 814 return value.convert(self.resolution) 815 816 def __iter__(self): 817 return iter(self.values) 818 819 def __len__(self): 820 return len(self.values) 821 822 def __getitem__(self, i): 823 return self.values[i] 824 825 def __setitem__(self, i, value): 826 self.values[i] = value 827 828 def __contains__(self, value): 829 test_value = self.convert(value) 830 return test_value in self.values 831 832 def append(self, value): 833 self.values.append(value) 834 835 def insert(self, i, value): 836 self.values.insert(i, value) 837 838 def pop(self): 839 return self.values.pop() 840 841 def insert_in_order(self, value): 842 bisect.insort_left(self, value) 843 844 def getDate(s): 845 846 "Parse the string 's', extracting and returning a date object." 847 848 dt = getDateTime(s) 849 if dt is not None: 850 return dt.as_date() 851 else: 852 return None 853 854 def getDateTime(s): 855 856 """ 857 Parse the string 's', extracting and returning a datetime object where time 858 information has been given or a date object where time information is 859 absent. 860 """ 861 862 m = datetime_regexp.search(s) 863 if m: 864 groups = list(m.groups()) 865 866 # Convert date and time data to integer or None. 867 868 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date() 869 else: 870 return None 871 872 def getDateFromCalendar(s): 873 874 """ 875 Parse the iCalendar format string 's', extracting and returning a date 876 object. 877 """ 878 879 dt = getDateTimeFromCalendar(s) 880 if dt is not None: 881 return dt.as_date() 882 else: 883 return None 884 885 def getDateTimeFromCalendar(s): 886 887 """ 888 Parse the iCalendar format datetime string 's', extracting and returning a 889 datetime object where time information has been given or a date object where 890 time information is absent. 891 """ 892 893 m = datetime_icalendar_regexp.search(s) 894 if m: 895 groups = list(m.groups()) 896 897 # Convert date and time data to integer or None. 898 899 return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or None]).as_datetime_or_date() 900 else: 901 return None 902 903 def getDateTimeFromISO8601(s): 904 905 """ 906 Parse the ISO 8601 format datetime string 's', returning a datetime object. 907 """ 908 909 m = datetime_iso8601_regexp.search(s) 910 if m: 911 groups = list(m.groups()) 912 913 # Convert date and time data to integer or None. 914 915 return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or m.group("zone")]).as_datetime_or_date() 916 else: 917 return None 918 919 def getDateTimeFromRFC2822(s): 920 921 """ 922 Parse the RFC 2822 format datetime string 's', returning a datetime object. 923 """ 924 925 data = parsedate_tz(s) 926 offset = data[9] 927 return DateTime(data[:6] + ("%02d:%02d" % getHoursAndMinutes(offset),)) 928 929 def getHoursAndMinutes(seconds): 930 931 "Return an (hours, minutes) tuple for the given number of 'seconds'." 932 933 hours = abs(seconds) / 3600 934 minutes = (abs(seconds) % 3600) / 60 935 return sign(seconds) * hours, sign(seconds) * minutes 936 937 def getDateStrings(s): 938 939 "Parse the string 's', extracting and returning all date strings." 940 941 start = 0 942 m = date_regexp.search(s, start) 943 l = [] 944 while m: 945 l.append("-".join(m.groups())) 946 m = date_regexp.search(s, m.end()) 947 return l 948 949 def getMonth(s): 950 951 "Parse the string 's', extracting and returning a month object." 952 953 m = month_regexp.search(s) 954 if m: 955 return Month(map(int, m.groups())) 956 else: 957 return None 958 959 def getCurrentDate(): 960 961 "Return the current date as a Date instance." 962 963 today = datetime.date.today() 964 return Date((today.year, today.month, today.day)) 965 966 def getCurrentMonth(): 967 968 "Return the current month as a Month instance." 969 970 today = datetime.date.today() 971 return Month((today.year, today.month)) 972 973 def getCurrentYear(): 974 975 "Return the current year." 976 977 today = datetime.date.today() 978 return today.year 979 980 def getCurrentTime(): 981 982 "Return the current time as a DateTime instance." 983 984 now = datetime.datetime.utcnow() 985 return DateTime((now.year, now.month, now.day, now.hour, now.minute, now.second, "UTC")) 986 987 # vim: tabstop=4 expandtab shiftwidth=4