1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/DateSupport.py Sun Jan 22 00:04:16 2012 +0100
1.3 @@ -0,0 +1,897 @@
1.4 +# -*- coding: iso-8859-1 -*-
1.5 +"""
1.6 + MoinMoin - DateSupport library (derived from EventAggregatorSupport)
1.7 +
1.8 + @copyright: 2008, 2009, 2010, 2011, 2012 by Paul Boddie <paul@boddie.org.uk>
1.9 + @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.10 +"""
1.11 +
1.12 +import calendar
1.13 +import datetime
1.14 +import re
1.15 +import bisect
1.16 +
1.17 +try:
1.18 + import pytz
1.19 +except ImportError:
1.20 + pytz = None
1.21 +
1.22 +__version__ = "0.1"
1.23 +
1.24 +# Date labels.
1.25 +
1.26 +month_labels = ["January", "February", "March", "April", "May", "June",
1.27 + "July", "August", "September", "October", "November", "December"]
1.28 +weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
1.29 +
1.30 +# Month, date, time and datetime parsing.
1.31 +
1.32 +month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})'
1.33 +date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})'
1.34 +time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?'
1.35 +timezone_offset_str = ur'(?P<offset>(UTC)?(?:(?P<sign>[-+])(?P<hours>[0-9]{2})(?::?(?P<minutes>[0-9]{2}))?))'
1.36 +timezone_olson_str = ur'(?P<olson>[a-zA-Z]+(?:/[-_a-zA-Z]+){1,2})'
1.37 +timezone_utc_str = ur'UTC'
1.38 +timezone_regexp_str = ur'(?P<zone>' + timezone_offset_str + '|' + timezone_olson_str + '|' + timezone_utc_str + ')'
1.39 +datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?'
1.40 +
1.41 +month_regexp = re.compile(month_regexp_str, re.UNICODE)
1.42 +date_regexp = re.compile(date_regexp_str, re.UNICODE)
1.43 +time_regexp = re.compile(time_regexp_str, re.UNICODE)
1.44 +timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE)
1.45 +timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE)
1.46 +datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE)
1.47 +
1.48 +# iCalendar date and datetime parsing.
1.49 +
1.50 +date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})'
1.51 +datetime_icalendar_regexp_str = date_icalendar_regexp_str + \
1.52 + ur'(?:' \
1.53 + ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \
1.54 + ur'(?P<utc>Z)?' \
1.55 + ur')?'
1.56 +
1.57 +date_icalendar_regexp = re.compile(date_icalendar_regexp_str, re.UNICODE)
1.58 +datetime_icalendar_regexp = re.compile(datetime_icalendar_regexp_str, re.UNICODE)
1.59 +
1.60 +# Utility functions.
1.61 +
1.62 +def int_or_none(x):
1.63 + if x is None:
1.64 + return x
1.65 + else:
1.66 + return int(x)
1.67 +
1.68 +def getMonthLabel(month):
1.69 +
1.70 + "Return an unlocalised label for the given 'month'."
1.71 +
1.72 + return month_labels[month - 1] # zero-based labels
1.73 +
1.74 +def getDayLabel(weekday):
1.75 +
1.76 + "Return an unlocalised label for the given 'weekday'."
1.77 +
1.78 + return weekday_labels[weekday]
1.79 +
1.80 +# Interfaces.
1.81 +
1.82 +class ActsAsTimespan:
1.83 + pass
1.84 +
1.85 +# Date-related functions.
1.86 +
1.87 +def cmp_dates_as_day_start(a, b):
1.88 +
1.89 + """
1.90 + Compare dates/datetimes 'a' and 'b' treating dates without time information
1.91 + as the earliest time in a particular day.
1.92 + """
1.93 +
1.94 + are_equal = a == b
1.95 +
1.96 + if are_equal:
1.97 + a2 = a.as_datetime_or_date()
1.98 + b2 = b.as_datetime_or_date()
1.99 +
1.100 + if isinstance(a2, Date) and isinstance(b2, DateTime):
1.101 + return -1
1.102 + elif isinstance(a2, DateTime) and isinstance(b2, Date):
1.103 + return 1
1.104 +
1.105 + return cmp(a, b)
1.106 +
1.107 +class Convertible:
1.108 +
1.109 + "Support for converting temporal objects."
1.110 +
1.111 + def _get_converter(self, resolution):
1.112 + if resolution == "month":
1.113 + return lambda x: x and x.as_month()
1.114 + elif resolution == "date":
1.115 + return lambda x: x and x.as_date()
1.116 + elif resolution == "datetime":
1.117 + return lambda x: x and x.as_datetime_or_date()
1.118 + else:
1.119 + return lambda x: x
1.120 +
1.121 +class Temporal(Convertible):
1.122 +
1.123 + "A simple temporal representation, common to dates and times."
1.124 +
1.125 + def __init__(self, data):
1.126 + self.data = list(data)
1.127 +
1.128 + def __repr__(self):
1.129 + return "%s(%r)" % (self.__class__.__name__, self.data)
1.130 +
1.131 + def __hash__(self):
1.132 + return hash(self.as_tuple())
1.133 +
1.134 + def as_tuple(self):
1.135 + return tuple(self.data)
1.136 +
1.137 + def convert(self, resolution):
1.138 + return self._get_converter(resolution)(self)
1.139 +
1.140 + def __cmp__(self, other):
1.141 +
1.142 + """
1.143 + The result of comparing this instance with 'other' is derived from a
1.144 + comparison of the instances' date(time) data at the highest common
1.145 + resolution, meaning that if a date is compared to a datetime, the
1.146 + datetime will be considered as a date. Thus, a date and a datetime
1.147 + referring to the same date will be considered equal.
1.148 + """
1.149 +
1.150 + if not isinstance(other, Temporal):
1.151 + return NotImplemented
1.152 + else:
1.153 + data = self.as_tuple()
1.154 + other_data = other.as_tuple()
1.155 + length = min(len(data), len(other_data))
1.156 + return cmp(data[:length], other_data[:length])
1.157 +
1.158 + def __sub__(self, other):
1.159 +
1.160 + """
1.161 + Return the difference between this object and the 'other' object at the
1.162 + highest common accuracy of both objects.
1.163 + """
1.164 +
1.165 + if not isinstance(other, Temporal):
1.166 + return NotImplemented
1.167 + else:
1.168 + data = self.as_tuple()
1.169 + other_data = other.as_tuple()
1.170 + if len(data) < len(other_data):
1.171 + return len(self.until(other))
1.172 + else:
1.173 + return len(other.until(self))
1.174 +
1.175 + def _until(self, start, end, nextfn, prevfn):
1.176 +
1.177 + """
1.178 + Return a collection of units of time by starting from the given 'start'
1.179 + and stepping across intervening units until 'end' is reached, using the
1.180 + given 'nextfn' and 'prevfn' to step from one unit to the next.
1.181 + """
1.182 +
1.183 + current = start
1.184 + units = [current]
1.185 + if current < end:
1.186 + while current < end:
1.187 + current = nextfn(current)
1.188 + units.append(current)
1.189 + elif current > end:
1.190 + while current > end:
1.191 + current = prevfn(current)
1.192 + units.append(current)
1.193 + return units
1.194 +
1.195 + def ambiguous(self):
1.196 +
1.197 + "Only times can be ambiguous."
1.198 +
1.199 + return 0
1.200 +
1.201 +class Month(Temporal):
1.202 +
1.203 + "A simple year-month representation."
1.204 +
1.205 + def __str__(self):
1.206 + return "%04d-%02d" % self.as_tuple()[:2]
1.207 +
1.208 + def as_datetime(self, day, hour, minute, second, zone):
1.209 + return DateTime(self.as_tuple() + (day, hour, minute, second, zone))
1.210 +
1.211 + def as_date(self, day):
1.212 + if day < 0:
1.213 + weekday, ndays = self.month_properties()
1.214 + day = ndays + 1 + day
1.215 + return Date(self.as_tuple() + (day,))
1.216 +
1.217 + def as_month(self):
1.218 + return self
1.219 +
1.220 + def year(self):
1.221 + return self.data[0]
1.222 +
1.223 + def month(self):
1.224 + return self.data[1]
1.225 +
1.226 + def month_properties(self):
1.227 +
1.228 + """
1.229 + Return the weekday of the 1st of the month, along with the number of
1.230 + days, as a tuple.
1.231 + """
1.232 +
1.233 + year, month = self.as_tuple()[:2]
1.234 + return calendar.monthrange(year, month)
1.235 +
1.236 + def month_update(self, n=1):
1.237 +
1.238 + "Return the month updated by 'n' months."
1.239 +
1.240 + year, month = self.as_tuple()[:2]
1.241 + return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1))
1.242 +
1.243 + update = month_update
1.244 +
1.245 + def next_month(self):
1.246 +
1.247 + "Return the month following this one."
1.248 +
1.249 + return self.month_update(1)
1.250 +
1.251 + next = next_month
1.252 +
1.253 + def previous_month(self):
1.254 +
1.255 + "Return the month preceding this one."
1.256 +
1.257 + return self.month_update(-1)
1.258 +
1.259 + previous = previous_month
1.260 +
1.261 + def months_until(self, end):
1.262 +
1.263 + "Return the collection of months from this month until 'end'."
1.264 +
1.265 + return self._until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month)
1.266 +
1.267 + until = months_until
1.268 +
1.269 +class Date(Month):
1.270 +
1.271 + "A simple year-month-day representation."
1.272 +
1.273 + def constrain(self):
1.274 + year, month, day = self.as_tuple()[:3]
1.275 +
1.276 + month = max(min(month, 12), 1)
1.277 + wd, last_day = calendar.monthrange(year, month)
1.278 + day = max(min(day, last_day), 1)
1.279 +
1.280 + self.data[1:3] = month, day
1.281 +
1.282 + def __str__(self):
1.283 + return "%04d-%02d-%02d" % self.as_tuple()[:3]
1.284 +
1.285 + def as_datetime(self, hour, minute, second, zone):
1.286 + return DateTime(self.as_tuple() + (hour, minute, second, zone))
1.287 +
1.288 + def as_start_of_day(self):
1.289 + return self.as_datetime(None, None, None, None)
1.290 +
1.291 + def as_date(self):
1.292 + return self
1.293 +
1.294 + def as_datetime_or_date(self):
1.295 + return self
1.296 +
1.297 + def as_month(self):
1.298 + return Month(self.data[:2])
1.299 +
1.300 + def day(self):
1.301 + return self.data[2]
1.302 +
1.303 + def day_update(self, n=1):
1.304 +
1.305 + "Return the month updated by 'n' days."
1.306 +
1.307 + delta = datetime.timedelta(n)
1.308 + dt = datetime.date(*self.as_tuple()[:3])
1.309 + dt_new = dt + delta
1.310 + return Date((dt_new.year, dt_new.month, dt_new.day))
1.311 +
1.312 + update = day_update
1.313 +
1.314 + def next_day(self):
1.315 +
1.316 + "Return the date following this one."
1.317 +
1.318 + year, month, day = self.as_tuple()[:3]
1.319 + _wd, end_day = calendar.monthrange(year, month)
1.320 + if day == end_day:
1.321 + if month == 12:
1.322 + return Date((year + 1, 1, 1))
1.323 + else:
1.324 + return Date((year, month + 1, 1))
1.325 + else:
1.326 + return Date((year, month, day + 1))
1.327 +
1.328 + next = next_day
1.329 +
1.330 + def previous_day(self):
1.331 +
1.332 + "Return the date preceding this one."
1.333 +
1.334 + year, month, day = self.as_tuple()[:3]
1.335 + if day == 1:
1.336 + if month == 1:
1.337 + return Date((year - 1, 12, 31))
1.338 + else:
1.339 + _wd, end_day = calendar.monthrange(year, month - 1)
1.340 + return Date((year, month - 1, end_day))
1.341 + else:
1.342 + return Date((year, month, day - 1))
1.343 +
1.344 + previous = previous_day
1.345 +
1.346 + def days_until(self, end):
1.347 +
1.348 + "Return the collection of days from this date until 'end'."
1.349 +
1.350 + return self._until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day)
1.351 +
1.352 + until = days_until
1.353 +
1.354 +class DateTime(Date):
1.355 +
1.356 + "A simple date plus time representation."
1.357 +
1.358 + def constrain(self):
1.359 + Date.constrain(self)
1.360 +
1.361 + hour, minute, second = self.as_tuple()[3:6]
1.362 +
1.363 + if self.has_time():
1.364 + hour = max(min(hour, 23), 0)
1.365 + minute = max(min(minute, 59), 0)
1.366 +
1.367 + if second is not None:
1.368 + second = max(min(second, 60), 0) # support leap seconds
1.369 +
1.370 + self.data[3:6] = hour, minute, second
1.371 +
1.372 + def __str__(self):
1.373 + return Date.__str__(self) + self.time_string()
1.374 +
1.375 + def time_string(self):
1.376 + if self.has_time():
1.377 + data = self.as_tuple()
1.378 + time_str = " %02d:%02d" % data[3:5]
1.379 + if data[5] is not None:
1.380 + time_str += ":%02d" % data[5]
1.381 + if data[6] is not None:
1.382 + time_str += " %s" % data[6]
1.383 + return time_str
1.384 + else:
1.385 + return ""
1.386 +
1.387 + def as_HTTP_datetime_string(self):
1.388 + weekday = calendar.weekday(*self.data[:3])
1.389 + return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ((
1.390 + getDayLabel(weekday),
1.391 + self.data[2],
1.392 + getMonthLabel(self.data[1]),
1.393 + self.data[0]
1.394 + ) + tuple(self.data[3:6]))
1.395 +
1.396 + def as_datetime(self):
1.397 + return self
1.398 +
1.399 + def as_date(self):
1.400 + return Date(self.data[:3])
1.401 +
1.402 + def as_datetime_or_date(self):
1.403 +
1.404 + """
1.405 + Return a date for this datetime if fields are missing. Otherwise, return
1.406 + this datetime itself.
1.407 + """
1.408 +
1.409 + if not self.has_time():
1.410 + return self.as_date()
1.411 + else:
1.412 + return self
1.413 +
1.414 + def __cmp__(self, other):
1.415 +
1.416 + """
1.417 + The result of comparing this instance with 'other' is, if both instances
1.418 + are datetime instances, derived from a comparison of the datetimes
1.419 + converted to UTC. If one or both datetimes cannot be converted to UTC,
1.420 + the datetimes are compared using the basic temporal comparison which
1.421 + compares their raw time data.
1.422 + """
1.423 +
1.424 + this = self
1.425 +
1.426 + if this.has_time():
1.427 + if isinstance(other, DateTime):
1.428 + if other.has_time():
1.429 + this_utc = this.to_utc()
1.430 + other_utc = other.to_utc()
1.431 + if this_utc is not None and other_utc is not None:
1.432 + return cmp(this_utc.as_tuple(), other_utc.as_tuple())
1.433 + else:
1.434 + other = other.padded()
1.435 + else:
1.436 + this = this.padded()
1.437 +
1.438 + return Date.__cmp__(this, other)
1.439 +
1.440 + def has_time(self):
1.441 +
1.442 + """
1.443 + Return whether this object has any time information. Objects without
1.444 + time information can refer to the very start of a day.
1.445 + """
1.446 +
1.447 + return self.data[3] is not None and self.data[4] is not None
1.448 +
1.449 + def time(self):
1.450 + return self.data[3:]
1.451 +
1.452 + def seconds(self):
1.453 + return self.data[5]
1.454 +
1.455 + def time_zone(self):
1.456 + return self.data[6]
1.457 +
1.458 + def set_time_zone(self, value):
1.459 + self.data[6] = value
1.460 +
1.461 + def padded(self, empty_value=0):
1.462 +
1.463 + """
1.464 + Return a datetime with missing fields defined as being the given
1.465 + 'empty_value' or 0 if not specified.
1.466 + """
1.467 +
1.468 + data = []
1.469 + for x in self.data[:6]:
1.470 + if x is None:
1.471 + data.append(empty_value)
1.472 + else:
1.473 + data.append(x)
1.474 +
1.475 + data += self.data[6:]
1.476 + return DateTime(data)
1.477 +
1.478 + def to_utc(self):
1.479 +
1.480 + """
1.481 + Return this object converted to UTC, or None if such a conversion is not
1.482 + defined.
1.483 + """
1.484 +
1.485 + if not self.has_time():
1.486 + return None
1.487 +
1.488 + offset = self.utc_offset()
1.489 + if offset:
1.490 + hours, minutes = offset
1.491 +
1.492 + # Invert the offset to get the correction.
1.493 +
1.494 + hours, minutes = -hours, -minutes
1.495 +
1.496 + # Get the components.
1.497 +
1.498 + hour, minute, second, zone = self.time()
1.499 + date = self.as_date()
1.500 +
1.501 + # Add the minutes and hours.
1.502 +
1.503 + minute += minutes
1.504 + if minute < 0 or minute > 59:
1.505 + hour += minute / 60
1.506 + minute = minute % 60
1.507 +
1.508 + # NOTE: This makes various assumptions and probably would not work
1.509 + # NOTE: for general arithmetic.
1.510 +
1.511 + hour += hours
1.512 + if hour < 0:
1.513 + date = date.previous_day()
1.514 + hour += 24
1.515 + elif hour > 23:
1.516 + date = date.next_day()
1.517 + hour -= 24
1.518 +
1.519 + return date.as_datetime(hour, minute, second, "UTC")
1.520 +
1.521 + # Cannot convert.
1.522 +
1.523 + else:
1.524 + return None
1.525 +
1.526 + def utc_offset(self):
1.527 +
1.528 + "Return the UTC offset in hours and minutes."
1.529 +
1.530 + zone = self.time_zone()
1.531 + if not zone:
1.532 + return None
1.533 +
1.534 + # Support explicit UTC zones.
1.535 +
1.536 + if zone == "UTC":
1.537 + return 0, 0
1.538 +
1.539 + # Attempt to return a UTC offset where an explicit offset has been set.
1.540 +
1.541 + match = timezone_offset_regexp.match(zone)
1.542 + if match:
1.543 + if match.group("sign") == "-":
1.544 + sign = -1
1.545 + else:
1.546 + sign = 1
1.547 +
1.548 + hours = int(match.group("hours")) * sign
1.549 + minutes = int(match.group("minutes") or 0) * sign
1.550 + return hours, minutes
1.551 +
1.552 + # Attempt to handle Olson time zone identifiers.
1.553 +
1.554 + dt = self.as_olson_datetime()
1.555 + if dt:
1.556 + seconds = dt.utcoffset().seconds
1.557 + hours = seconds / 3600
1.558 + minutes = (seconds % 3600) / 60
1.559 + return hours, minutes
1.560 +
1.561 + # Otherwise return None.
1.562 +
1.563 + return None
1.564 +
1.565 + def olson_identifier(self):
1.566 +
1.567 + "Return the Olson identifier from any zone information."
1.568 +
1.569 + zone = self.time_zone()
1.570 + if not zone:
1.571 + return None
1.572 +
1.573 + # Attempt to match an identifier.
1.574 +
1.575 + match = timezone_olson_regexp.match(zone)
1.576 + if match:
1.577 + return match.group("olson")
1.578 + else:
1.579 + return None
1.580 +
1.581 + def _as_olson_datetime(self, hours=None):
1.582 +
1.583 + """
1.584 + Return a Python datetime object for this datetime interpreted using any
1.585 + Olson time zone identifier and the given 'hours' offset, raising one of
1.586 + the pytz exceptions in case of ambiguity.
1.587 + """
1.588 +
1.589 + olson = self.olson_identifier()
1.590 + if olson and pytz:
1.591 + tz = pytz.timezone(olson)
1.592 + data = self.padded().as_tuple()[:6]
1.593 + dt = datetime.datetime(*data)
1.594 +
1.595 + # With an hours offset, find a time probably in a previously
1.596 + # applicable time zone.
1.597 +
1.598 + if hours is not None:
1.599 + td = datetime.timedelta(0, hours * 3600)
1.600 + dt += td
1.601 +
1.602 + ldt = tz.localize(dt, None)
1.603 +
1.604 + # With an hours offset, adjust the time to define it within the
1.605 + # previously applicable time zone but at the presumably intended
1.606 + # position.
1.607 +
1.608 + if hours is not None:
1.609 + ldt -= td
1.610 +
1.611 + return ldt
1.612 + else:
1.613 + return None
1.614 +
1.615 + def as_olson_datetime(self):
1.616 +
1.617 + """
1.618 + Return a Python datetime object for this datetime interpreted using any
1.619 + Olson time zone identifier, choosing the time from the zone before the
1.620 + period of ambiguity.
1.621 + """
1.622 +
1.623 + try:
1.624 + return self._as_olson_datetime()
1.625 + except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError):
1.626 +
1.627 + # Try again, using an earlier local time and then stepping forward
1.628 + # in the chosen zone.
1.629 + # NOTE: Four hours earlier seems reasonable.
1.630 +
1.631 + return self._as_olson_datetime(-4)
1.632 +
1.633 + def ambiguous(self):
1.634 +
1.635 + "Return whether the time is local and ambiguous."
1.636 +
1.637 + try:
1.638 + self._as_olson_datetime()
1.639 + except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError):
1.640 + return 1
1.641 +
1.642 + return 0
1.643 +
1.644 +class Timespan(ActsAsTimespan, Convertible):
1.645 +
1.646 + """
1.647 + A period of time which can be compared against others to check for overlaps.
1.648 + """
1.649 +
1.650 + def __init__(self, start, end):
1.651 + self.start = start
1.652 + self.end = end
1.653 +
1.654 + # NOTE: Should perhaps catch ambiguous time problems elsewhere.
1.655 +
1.656 + if self.ambiguous() and self.start is not None and self.end is not None and start > end:
1.657 + self.start, self.end = end, start
1.658 +
1.659 + def __repr__(self):
1.660 + return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end)
1.661 +
1.662 + def __hash__(self):
1.663 + return hash((self.start, self.end))
1.664 +
1.665 + def as_timespan(self):
1.666 + return self
1.667 +
1.668 + def as_limits(self):
1.669 + return self.start, self.end
1.670 +
1.671 + def ambiguous(self):
1.672 + return self.start is not None and self.start.ambiguous() or self.end is not None and self.end.ambiguous()
1.673 +
1.674 + def convert(self, resolution):
1.675 + return Timespan(*map(self._get_converter(resolution), self.as_limits()))
1.676 +
1.677 + def is_before(self, a, b):
1.678 +
1.679 + """
1.680 + Return whether 'a' is before 'b'. Since the end datetime of one period
1.681 + may be the same as the start datetime of another period, and yet the
1.682 + first period is intended to be concluded by the end datetime and not
1.683 + overlap with the other period, a different test is employed for datetime
1.684 + comparisons.
1.685 + """
1.686 +
1.687 + # Datetimes without times can be equal to dates and be considered as
1.688 + # occurring before those dates. Generally, datetimes should not be
1.689 + # produced without time information as getDateTime converts such
1.690 + # datetimes to dates.
1.691 +
1.692 + if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()):
1.693 + return a <= b
1.694 + else:
1.695 + return a < b
1.696 +
1.697 + def __contains__(self, other):
1.698 +
1.699 + """
1.700 + This instance is considered to contain 'other' if one is not before or
1.701 + after the other. If this instance overlaps or coincides with 'other',
1.702 + then 'other' is regarded as belonging to this instance's time period.
1.703 + """
1.704 +
1.705 + return self == other
1.706 +
1.707 + def __cmp__(self, other):
1.708 +
1.709 + """
1.710 + Return whether this timespan occupies the same period of time as the
1.711 + 'other'. Timespans are considered less than others if their end points
1.712 + precede the other's start point, and are considered greater than others
1.713 + if their start points follow the other's end point.
1.714 + """
1.715 +
1.716 + if isinstance(other, ActsAsTimespan):
1.717 + other = other.as_timespan()
1.718 +
1.719 + if self.end is not None and other.start is not None and self.is_before(self.end, other.start):
1.720 + return -1
1.721 + elif self.start is not None and other.end is not None and self.is_before(other.end, self.start):
1.722 + return 1
1.723 + else:
1.724 + return 0
1.725 +
1.726 + else:
1.727 + if self.end is not None and self.is_before(self.end, other):
1.728 + return -1
1.729 + elif self.start is not None and self.is_before(other, self.start):
1.730 + return 1
1.731 + else:
1.732 + return 0
1.733 +
1.734 +class TimespanCollection:
1.735 +
1.736 + """
1.737 + A class providing a list-like interface supporting membership tests at a
1.738 + particular resolution in order to maintain a collection of non-overlapping
1.739 + timespans.
1.740 + """
1.741 +
1.742 + def __init__(self, resolution, values=None):
1.743 + self.resolution = resolution
1.744 + self.values = values or []
1.745 +
1.746 + def as_timespan(self):
1.747 + return Timespan(*self.as_limits())
1.748 +
1.749 + def as_limits(self):
1.750 +
1.751 + "Return the earliest and latest points in time for this collection."
1.752 +
1.753 + if not self.values:
1.754 + return None, None
1.755 + else:
1.756 + first, last = self.values[0], self.values[-1]
1.757 + if isinstance(first, ActsAsTimespan):
1.758 + first = first.as_timespan().start
1.759 + if isinstance(last, ActsAsTimespan):
1.760 + last = last.as_timespan().end
1.761 + return first, last
1.762 +
1.763 + def convert(self, value):
1.764 + if isinstance(value, ActsAsTimespan):
1.765 + ts = value.as_timespan()
1.766 + return ts and ts.convert(self.resolution)
1.767 + else:
1.768 + return value.convert(self.resolution)
1.769 +
1.770 + def __iter__(self):
1.771 + return iter(self.values)
1.772 +
1.773 + def __len__(self):
1.774 + return len(self.values)
1.775 +
1.776 + def __getitem__(self, i):
1.777 + return self.values[i]
1.778 +
1.779 + def __setitem__(self, i, value):
1.780 + self.values[i] = value
1.781 +
1.782 + def __contains__(self, value):
1.783 + test_value = self.convert(value)
1.784 + return test_value in self.values
1.785 +
1.786 + def append(self, value):
1.787 + self.values.append(value)
1.788 +
1.789 + def insert(self, i, value):
1.790 + self.values.insert(i, value)
1.791 +
1.792 + def pop(self):
1.793 + return self.values.pop()
1.794 +
1.795 + def insert_in_order(self, value):
1.796 + bisect.insort_left(self, value)
1.797 +
1.798 +def getDate(s):
1.799 +
1.800 + "Parse the string 's', extracting and returning a date object."
1.801 +
1.802 + dt = getDateTime(s)
1.803 + if dt is not None:
1.804 + return dt.as_date()
1.805 + else:
1.806 + return None
1.807 +
1.808 +def getDateTime(s):
1.809 +
1.810 + """
1.811 + Parse the string 's', extracting and returning a datetime object where time
1.812 + information has been given or a date object where time information is
1.813 + absent.
1.814 + """
1.815 +
1.816 + m = datetime_regexp.search(s)
1.817 + if m:
1.818 + groups = list(m.groups())
1.819 +
1.820 + # Convert date and time data to integer or None.
1.821 +
1.822 + return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date()
1.823 + else:
1.824 + return None
1.825 +
1.826 +def getDateFromCalendar(s):
1.827 +
1.828 + """
1.829 + Parse the iCalendar format string 's', extracting and returning a date
1.830 + object.
1.831 + """
1.832 +
1.833 + dt = getDateTimeFromCalendar(s)
1.834 + if dt is not None:
1.835 + return dt.as_date()
1.836 + else:
1.837 + return None
1.838 +
1.839 +def getDateTimeFromCalendar(s):
1.840 +
1.841 + """
1.842 + Parse the iCalendar format datetime string 's', extracting and returning a
1.843 + datetime object where time information has been given or a date object where
1.844 + time information is absent.
1.845 + """
1.846 +
1.847 + m = datetime_icalendar_regexp.search(s)
1.848 + if m:
1.849 + groups = list(m.groups())
1.850 +
1.851 + # Convert date and time data to integer or None.
1.852 +
1.853 + return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or None]).as_datetime_or_date()
1.854 + else:
1.855 + return None
1.856 +
1.857 +def getDateStrings(s):
1.858 +
1.859 + "Parse the string 's', extracting and returning all date strings."
1.860 +
1.861 + start = 0
1.862 + m = date_regexp.search(s, start)
1.863 + l = []
1.864 + while m:
1.865 + l.append("-".join(m.groups()))
1.866 + m = date_regexp.search(s, m.end())
1.867 + return l
1.868 +
1.869 +def getMonth(s):
1.870 +
1.871 + "Parse the string 's', extracting and returning a month object."
1.872 +
1.873 + m = month_regexp.search(s)
1.874 + if m:
1.875 + return Month(map(int, m.groups()))
1.876 + else:
1.877 + return None
1.878 +
1.879 +def getCurrentDate():
1.880 +
1.881 + "Return the current date as a (year, month, day) tuple."
1.882 +
1.883 + today = datetime.date.today()
1.884 + return Date((today.year, today.month, today.day))
1.885 +
1.886 +def getCurrentMonth():
1.887 +
1.888 + "Return the current month as a (year, month) tuple."
1.889 +
1.890 + today = datetime.date.today()
1.891 + return Month((today.year, today.month))
1.892 +
1.893 +def getCurrentYear():
1.894 +
1.895 + "Return the current year."
1.896 +
1.897 + today = datetime.date.today()
1.898 + return today.year
1.899 +
1.900 +# vim: tabstop=4 expandtab shiftwidth=4