1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - DateSupport library (derived from EventAggregatorSupport) 4 5 @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 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=" ", zone_separator=":"): 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%s%02d" % (zone_prefix, utc_offset[0], zone_separator, utc_offset[1]) 406 else: 407 time_str += "%s%s" % (zone_prefix, data[6]) 408 return time_str 409 else: 410 return "" 411 412 def as_RFC2822_datetime_string(self): 413 weekday = calendar.weekday(*self.data[:3]) 414 return "%s, %02d %s %04d %s" % ( 415 getDayLabel(weekday)[:3], 416 self.data[2], 417 getMonthLabel(self.data[1])[:3], 418 self.data[0], 419 self.time_string(zone_as_offset=True, time_prefix="", zone_prefix=" ", zone_separator="") 420 ) 421 422 def as_HTTP_datetime_string(self): 423 weekday = calendar.weekday(*self.data[:3]) 424 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (( 425 getDayLabel(weekday), 426 self.data[2], 427 getMonthLabel(self.data[1]), 428 self.data[0] 429 ) + tuple(self.data[3:6])) 430 431 def as_ISO8601_datetime_string(self): 432 return Date.__str__(self) + self.time_string(zone_as_offset=True, time_prefix="T", zone_prefix="") 433 434 def as_datetime(self): 435 return self 436 437 def as_date(self): 438 return Date(self.data[:3]) 439 440 def as_datetime_or_date(self): 441 442 """ 443 Return a date for this datetime if fields are missing. Otherwise, return 444 this datetime itself. 445 """ 446 447 if not self.has_time(): 448 return self.as_date() 449 else: 450 return self 451 452 def __cmp__(self, other): 453 454 """ 455 The result of comparing this instance with 'other' is, if both instances 456 are datetime instances, derived from a comparison of the datetimes 457 converted to UTC. If one or both datetimes cannot be converted to UTC, 458 the datetimes are compared using the basic temporal comparison which 459 compares their raw time data. 460 """ 461 462 this = self 463 464 if this.has_time(): 465 if isinstance(other, DateTime): 466 if other.has_time(): 467 this_utc = this.to_utc() 468 other_utc = other.to_utc() 469 if this_utc is not None and other_utc is not None: 470 return cmp(this_utc.as_tuple(), other_utc.as_tuple()) 471 else: 472 other = other.padded() 473 else: 474 this = this.padded() 475 476 return Date.__cmp__(this, other) 477 478 def __sub__(self, other): 479 480 """ 481 Return the difference between this object and the 'other' object at the 482 highest common accuracy of both objects. 483 """ 484 485 if not isinstance(other, Temporal): 486 return NotImplemented 487 elif not other.has_time(): 488 return self.as_date() - other 489 else: 490 utc = self.to_utc() 491 other = other.to_utc() 492 days = utc.as_date() - other.as_date() 493 h1, m1, s1 = utc.as_tuple()[3:6] 494 h2, m2, s2 = other.as_tuple()[3:6] 495 return days * 24 * 3600 + (h1 - h2) * 3600 + (m1 - m2) * 60 + s1 - s2 496 497 def has_time(self): 498 499 """ 500 Return whether this object has any time information. Objects without 501 time information can refer to the very start of a day. 502 """ 503 504 return self.data[3] is not None and self.data[4] is not None 505 506 def time(self): 507 return self.data[3:] 508 509 def seconds(self): 510 return self.data[5] 511 512 def time_zone(self): 513 return self.data[6] 514 515 def set_time_zone(self, value): 516 self.data[6] = value 517 518 def padded(self, empty_value=0): 519 520 """ 521 Return a datetime with missing fields defined as being the given 522 'empty_value' or 0 if not specified. 523 """ 524 525 data = [] 526 for x in self.data[:6]: 527 if x is None: 528 data.append(empty_value) 529 else: 530 data.append(x) 531 532 data += self.data[6:] 533 return DateTime(data) 534 535 def to_utc(self): 536 537 """ 538 Return this object converted to UTC, or None if such a conversion is not 539 defined. 540 """ 541 542 if not self.has_time(): 543 return None 544 545 offset = self.utc_offset() 546 if offset: 547 hours, minutes = offset 548 549 # Invert the offset to get the correction. 550 551 hours, minutes = -hours, -minutes 552 553 # Get the components. 554 555 hour, minute, second, zone = self.time() 556 date = self.as_date() 557 558 # Add the minutes and hours. 559 560 minute += minutes 561 if minute < 0 or minute > 59: 562 hour += minute / 60 563 minute = minute % 60 564 565 # NOTE: This makes various assumptions and probably would not work 566 # NOTE: for general arithmetic. 567 568 hour += hours 569 if hour < 0: 570 date = date.previous_day() 571 hour += 24 572 elif hour > 23: 573 date = date.next_day() 574 hour -= 24 575 576 return date.as_datetime(hour, minute, second, "UTC") 577 578 # Cannot convert. 579 580 else: 581 return None 582 583 def utc_offset(self): 584 585 "Return the UTC offset in hours and minutes." 586 587 zone = self.time_zone() 588 if not zone: 589 return None 590 591 # Support explicit UTC zones. 592 593 if zone == "UTC": 594 return 0, 0 595 596 # Attempt to return a UTC offset where an explicit offset has been set. 597 598 match = timezone_offset_regexp.match(zone) 599 if match: 600 if match.group("sign") == "-": 601 offset_sign = -1 602 else: 603 offset_sign = 1 604 605 hours = int(match.group("hours")) * offset_sign 606 minutes = int(match.group("minutes") or 0) * offset_sign 607 return hours, minutes 608 609 # Attempt to handle Olson time zone identifiers. 610 611 dt = self.as_olson_datetime() 612 if dt: 613 seconds = dt.utcoffset().seconds + dt.utcoffset().days * 24 * 3600 614 return getHoursAndMinutes(seconds) 615 616 # Otherwise return None. 617 618 return None 619 620 def olson_identifier(self): 621 622 "Return the Olson identifier from any zone information." 623 624 zone = self.time_zone() 625 if not zone: 626 return None 627 628 # Attempt to match an identifier. 629 630 match = timezone_olson_regexp.match(zone) 631 if match: 632 return match.group("olson") 633 else: 634 return None 635 636 def _as_olson_datetime(self, hours=None): 637 638 """ 639 Return a Python datetime object for this datetime interpreted using any 640 Olson time zone identifier and the given 'hours' offset, raising one of 641 the pytz exceptions in case of ambiguity. 642 """ 643 644 olson = self.olson_identifier() 645 if olson: 646 tz = pytz.timezone(olson) 647 data = self.padded().as_tuple()[:6] 648 dt = datetime.datetime(*data) 649 650 # With an hours offset, find a time probably in a previously 651 # applicable time zone. 652 653 if hours is not None: 654 td = datetime.timedelta(0, hours * 3600) 655 dt += td 656 657 ldt = tz.localize(dt, None) 658 659 # With an hours offset, adjust the time to define it within the 660 # previously applicable time zone but at the presumably intended 661 # position. 662 663 if hours is not None: 664 ldt -= td 665 666 return ldt 667 else: 668 return None 669 670 def as_olson_datetime(self): 671 672 """ 673 Return a Python datetime object for this datetime interpreted using any 674 Olson time zone identifier, choosing the time from the zone before the 675 period of ambiguity. 676 """ 677 678 self._check_pytz() 679 680 try: 681 return self._as_olson_datetime() 682 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError, pytz.NonExistentTimeError): 683 684 # Try again, using an earlier local time and then stepping forward 685 # in the chosen zone. 686 # NOTE: Four hours earlier seems reasonable. 687 688 return self._as_olson_datetime(-4) 689 690 def ambiguous(self): 691 692 "Return whether the time is local and ambiguous." 693 694 self._check_pytz() 695 696 try: 697 self._as_olson_datetime() 698 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError, pytz.NonExistentTimeError): 699 return 1 700 701 return 0 702 703 def _check_pytz(self): 704 if not pytz: 705 raise NotImplementedError, "pytz must be installed for Olson " \ 706 "time zones to be supported" 707 708 class Timespan(ActsAsTimespan, Convertible): 709 710 """ 711 A period of time which can be compared against others to check for overlaps. 712 """ 713 714 def __init__(self, start, end): 715 self.start = start 716 self.end = end 717 718 # NOTE: Should perhaps catch ambiguous time problems elsewhere. 719 720 if self.ambiguous() and self.start is not None and self.end is not None and start > end: 721 self.start, self.end = end, start 722 723 def __repr__(self): 724 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 725 726 def __hash__(self): 727 return hash((self.start, self.end)) 728 729 def as_timespan(self): 730 return self 731 732 def as_limits(self): 733 return self.start, self.end 734 735 def ambiguous(self): 736 return self.start is not None and self.start.ambiguous() or self.end is not None and self.end.ambiguous() 737 738 def convert(self, resolution): 739 return Timespan(*map(self._get_converter(resolution), self.as_limits())) 740 741 def is_before(self, a, b): 742 743 """ 744 Return whether 'a' is before 'b'. Since the end datetime of one period 745 may be the same as the start datetime of another period, and yet the 746 first period is intended to be concluded by the end datetime and not 747 overlap with the other period, a different test is employed for datetime 748 comparisons. 749 """ 750 751 # Datetimes without times can be equal to dates and be considered as 752 # occurring before those dates. Generally, datetimes should not be 753 # produced without time information as getDateTime converts such 754 # datetimes to dates. 755 756 if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()): 757 return a <= b 758 else: 759 return a < b 760 761 def __contains__(self, other): 762 763 """ 764 This instance is considered to contain 'other' if one is not before or 765 after the other. If this instance overlaps or coincides with 'other', 766 then 'other' is regarded as belonging to this instance's time period. 767 """ 768 769 return self == other 770 771 def __cmp__(self, other): 772 773 """ 774 Return whether this timespan occupies the same period of time as the 775 'other'. Timespans are considered less than others if their end points 776 precede the other's start point, and are considered greater than others 777 if their start points follow the other's end point. 778 """ 779 780 if isinstance(other, ActsAsTimespan): 781 other = other.as_timespan() 782 783 before = self.end is not None and other.start is not None and self.is_before(self.end, other.start) 784 after = self.start is not None and other.end is not None and self.is_before(other.end, self.start) 785 else: 786 before = self.end is not None and self.is_before(self.end, other) 787 after = self.start is not None and self.is_before(other, self.start) 788 789 # Two identical points in time will be "before" each other according to 790 # the is_before test. 791 792 if not before and not after or before and after: 793 return 0 794 elif before: 795 return -1 796 else: 797 return 1 798 799 class TimespanCollection: 800 801 """ 802 A class providing a list-like interface supporting membership tests at a 803 particular resolution in order to maintain a collection of non-overlapping 804 timespans. 805 """ 806 807 def __init__(self, resolution, values=None): 808 self.resolution = resolution 809 self.values = values or [] 810 811 def as_timespan(self): 812 return Timespan(*self.as_limits()) 813 814 def as_limits(self): 815 816 "Return the earliest and latest points in time for this collection." 817 818 if not self.values: 819 return None, None 820 else: 821 first, last = self.values[0], self.values[-1] 822 if isinstance(first, ActsAsTimespan): 823 first = first.as_timespan().start 824 if isinstance(last, ActsAsTimespan): 825 last = last.as_timespan().end 826 return first, last 827 828 def convert(self, value): 829 if isinstance(value, ActsAsTimespan): 830 ts = value.as_timespan() 831 return ts and ts.convert(self.resolution) 832 else: 833 return value.convert(self.resolution) 834 835 def __iter__(self): 836 return iter(self.values) 837 838 def __len__(self): 839 return len(self.values) 840 841 def __getitem__(self, i): 842 return self.values[i] 843 844 def __setitem__(self, i, value): 845 self.values[i] = value 846 847 def __contains__(self, value): 848 test_value = self.convert(value) 849 return test_value in self.values 850 851 def append(self, value): 852 self.values.append(value) 853 854 def insert(self, i, value): 855 self.values.insert(i, value) 856 857 def pop(self): 858 return self.values.pop() 859 860 def insert_in_order(self, value): 861 bisect.insort_left(self, value) 862 863 def getDate(s): 864 865 "Parse the string 's', extracting and returning a date object." 866 867 dt = getDateTime(s) 868 if dt is not None: 869 return dt.as_date() 870 else: 871 return None 872 873 def getDateTime(s): 874 875 """ 876 Parse the string 's', extracting and returning a datetime object where time 877 information has been given or a date object where time information is 878 absent. 879 """ 880 881 m = datetime_regexp.search(s) 882 if m: 883 groups = list(m.groups()) 884 885 # Convert date and time data to integer or None. 886 887 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date() 888 else: 889 return None 890 891 def getDateFromCalendar(s): 892 893 """ 894 Parse the iCalendar format string 's', extracting and returning a date 895 object. 896 """ 897 898 dt = getDateTimeFromCalendar(s) 899 if dt is not None: 900 return dt.as_date() 901 else: 902 return None 903 904 def getDateTimeFromCalendar(s): 905 906 """ 907 Parse the iCalendar format datetime string 's', extracting and returning a 908 datetime object where time information has been given or a date object where 909 time information is absent. 910 """ 911 912 m = datetime_icalendar_regexp.search(s) 913 if m: 914 groups = list(m.groups()) 915 916 # Convert date and time data to integer or None. 917 918 return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or None]).as_datetime_or_date() 919 else: 920 return None 921 922 def getDateTimeFromISO8601(s): 923 924 """ 925 Parse the ISO 8601 format datetime string 's', returning a datetime object. 926 """ 927 928 m = datetime_iso8601_regexp.search(s) 929 if m: 930 groups = list(m.groups()) 931 932 # Convert date and time data to integer or None. 933 934 return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or m.group("zone")]).as_datetime_or_date() 935 else: 936 return None 937 938 def getDateTimeFromRFC2822(s): 939 940 """ 941 Parse the RFC 2822 format datetime string 's', returning a datetime object. 942 """ 943 944 data = parsedate_tz(s) 945 offset = data[9] 946 return DateTime(data[:6] + ("%02d:%02d" % getHoursAndMinutes(offset),)) 947 948 def getHoursAndMinutes(seconds): 949 950 "Return an (hours, minutes) tuple for the given number of 'seconds'." 951 952 hours = abs(seconds) / 3600 953 minutes = (abs(seconds) % 3600) / 60 954 return sign(seconds) * hours, sign(seconds) * minutes 955 956 def getDateStrings(s): 957 958 "Parse the string 's', extracting and returning all date strings." 959 960 start = 0 961 m = date_regexp.search(s, start) 962 l = [] 963 while m: 964 l.append("-".join(m.groups())) 965 m = date_regexp.search(s, m.end()) 966 return l 967 968 def getMonth(s): 969 970 "Parse the string 's', extracting and returning a month object." 971 972 m = month_regexp.search(s) 973 if m: 974 return Month(map(int, m.groups())) 975 else: 976 return None 977 978 def getCurrentDate(): 979 980 "Return the current date as a Date instance." 981 982 today = datetime.date.today() 983 return Date((today.year, today.month, today.day)) 984 985 def getCurrentMonth(): 986 987 "Return the current month as a Month instance." 988 989 today = datetime.date.today() 990 return Month((today.year, today.month)) 991 992 def getCurrentYear(): 993 994 "Return the current year." 995 996 today = datetime.date.today() 997 return today.year 998 999 def getCurrentTime(): 1000 1001 "Return the current time as a DateTime instance." 1002 1003 now = datetime.datetime.utcnow() 1004 return DateTime((now.year, now.month, now.day, now.hour, now.minute, now.second, "UTC")) 1005 1006 # vim: tabstop=4 expandtab shiftwidth=4