123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- # Copyright 2010 Matt Chaput. All rights reserved.
- #
- # Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions are met:
- #
- # 1. Redistributions of source code must retain the above copyright notice,
- # this list of conditions and the following disclaimer.
- #
- # 2. Redistributions in binary form must reproduce the above copyright
- # notice, this list of conditions and the following disclaimer in the
- # documentation and/or other materials provided with the distribution.
- #
- # THIS SOFTWARE IS PROVIDED BY MATT CHAPUT ``AS IS'' AND ANY EXPRESS OR
- # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- # EVENT SHALL MATT CHAPUT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
- # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
- # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- #
- # The views and conclusions contained in the software and documentation are
- # those of the authors and should not be interpreted as representing official
- # policies, either expressed or implied, of Matt Chaput.
- import calendar
- import copy
- from datetime import date, datetime, timedelta
- from whoosh.compat import iteritems
- class TimeError(Exception):
- pass
- def relative_days(current_wday, wday, dir):
- """Returns the number of days (positive or negative) to the "next" or
- "last" of a certain weekday. ``current_wday`` and ``wday`` are numbers,
- i.e. 0 = monday, 1 = tuesday, 2 = wednesday, etc.
- >>> # Get the number of days to the next tuesday, if today is Sunday
- >>> relative_days(6, 1, 1)
- 2
- :param current_wday: the number of the current weekday.
- :param wday: the target weekday.
- :param dir: -1 for the "last" (past) weekday, 1 for the "next" (future)
- weekday.
- """
- if current_wday == wday:
- return 7 * dir
- if dir == 1:
- return (wday + 7 - current_wday) % 7
- else:
- return (current_wday + 7 - wday) % 7 * -1
- def timedelta_to_usecs(td):
- total = td.days * 86400000000 # Microseconds in a day
- total += td.seconds * 1000000 # Microseconds in a second
- total += td.microseconds
- return total
- def datetime_to_long(dt):
- """Converts a datetime object to a long integer representing the number
- of microseconds since ``datetime.min``.
- """
- return timedelta_to_usecs(dt.replace(tzinfo=None) - dt.min)
- def long_to_datetime(x):
- """Converts a long integer representing the number of microseconds since
- ``datetime.min`` to a datetime object.
- """
- days = x // 86400000000 # Microseconds in a day
- x -= days * 86400000000
- seconds = x // 1000000 # Microseconds in a second
- x -= seconds * 1000000
- return datetime.min + timedelta(days=days, seconds=seconds, microseconds=x)
- # Ambiguous datetime object
- class adatetime(object):
- """An "ambiguous" datetime object. This object acts like a
- ``datetime.datetime`` object but can have any of its attributes set to
- None, meaning unspecified.
- """
- units = frozenset(("year", "month", "day", "hour", "minute", "second",
- "microsecond"))
- def __init__(self, year=None, month=None, day=None, hour=None, minute=None,
- second=None, microsecond=None):
- if isinstance(year, datetime):
- dt = year
- self.year, self.month, self.day = dt.year, dt.month, dt.day
- self.hour, self.minute, self.second = dt.hour, dt.minute, dt.second
- self.microsecond = dt.microsecond
- else:
- if month is not None and (month < 1 or month > 12):
- raise TimeError("month must be in 1..12")
- if day is not None and day < 1:
- raise TimeError("day must be greater than 1")
- if (year is not None and month is not None and day is not None
- and day > calendar.monthrange(year, month)[1]):
- raise TimeError("day is out of range for month")
- if hour is not None and (hour < 0 or hour > 23):
- raise TimeError("hour must be in 0..23")
- if minute is not None and (minute < 0 or minute > 59):
- raise TimeError("minute must be in 0..59")
- if second is not None and (second < 0 or second > 59):
- raise TimeError("second must be in 0..59")
- if microsecond is not None and (microsecond < 0
- or microsecond > 999999):
- raise TimeError("microsecond must be in 0..999999")
- self.year, self.month, self.day = year, month, day
- self.hour, self.minute, self.second = hour, minute, second
- self.microsecond = microsecond
- def __eq__(self, other):
- if not other.__class__ is self.__class__:
- if not is_ambiguous(self) and isinstance(other, datetime):
- return fix(self) == other
- else:
- return False
- return all(getattr(self, unit) == getattr(other, unit)
- for unit in self.units)
- def __repr__(self):
- return "%s%r" % (self.__class__.__name__, self.tuple())
- def tuple(self):
- """Returns the attributes of the ``adatetime`` object as a tuple of
- ``(year, month, day, hour, minute, second, microsecond)``.
- """
- return (self.year, self.month, self.day, self.hour, self.minute,
- self.second, self.microsecond)
- def date(self):
- return date(self.year, self.month, self.day)
- def copy(self):
- return adatetime(year=self.year, month=self.month, day=self.day,
- hour=self.hour, minute=self.minute, second=self.second,
- microsecond=self.microsecond)
- def replace(self, **kwargs):
- """Returns a copy of this object with the attributes given as keyword
- arguments replaced.
- >>> adt = adatetime(year=2009, month=10, day=31)
- >>> adt.replace(year=2010)
- (2010, 10, 31, None, None, None, None)
- """
- newadatetime = self.copy()
- for key, value in iteritems(kwargs):
- if key in self.units:
- setattr(newadatetime, key, value)
- else:
- raise KeyError("Unknown argument %r" % key)
- return newadatetime
- def floor(self):
- """Returns a ``datetime`` version of this object with all unspecified
- (None) attributes replaced by their lowest values.
- This method raises an error if the ``adatetime`` object has no year.
- >>> adt = adatetime(year=2009, month=5)
- >>> adt.floor()
- datetime.datetime(2009, 5, 1, 0, 0, 0, 0)
- """
- y, m, d, h, mn, s, ms = (self.year, self.month, self.day, self.hour,
- self.minute, self.second, self.microsecond)
- if y is None:
- raise ValueError("Date has no year")
- if m is None:
- m = 1
- if d is None:
- d = 1
- if h is None:
- h = 0
- if mn is None:
- mn = 0
- if s is None:
- s = 0
- if ms is None:
- ms = 0
- return datetime(y, m, d, h, mn, s, ms)
- def ceil(self):
- """Returns a ``datetime`` version of this object with all unspecified
- (None) attributes replaced by their highest values.
- This method raises an error if the ``adatetime`` object has no year.
- >>> adt = adatetime(year=2009, month=5)
- >>> adt.floor()
- datetime.datetime(2009, 5, 30, 23, 59, 59, 999999)
- """
- y, m, d, h, mn, s, ms = (self.year, self.month, self.day, self.hour,
- self.minute, self.second, self.microsecond)
- if y is None:
- raise ValueError("Date has no year")
- if m is None:
- m = 12
- if d is None:
- d = calendar.monthrange(y, m)[1]
- if h is None:
- h = 23
- if mn is None:
- mn = 59
- if s is None:
- s = 59
- if ms is None:
- ms = 999999
- return datetime(y, m, d, h, mn, s, ms)
- def disambiguated(self, basedate):
- """Returns either a ``datetime`` or unambiguous ``timespan`` version
- of this object.
- Unless this ``adatetime`` object is full specified down to the
- microsecond, this method will return a timespan built from the "floor"
- and "ceil" of this object.
- This method raises an error if the ``adatetime`` object has no year.
- >>> adt = adatetime(year=2009, month=10, day=31)
- >>> adt.disambiguated()
- timespan(datetime(2009, 10, 31, 0, 0, 0, 0), datetime(2009, 10, 31, 23, 59 ,59, 999999)
- """
- dt = self
- if not is_ambiguous(dt):
- return fix(dt)
- return timespan(dt, dt).disambiguated(basedate)
- # Time span class
- class timespan(object):
- """A span of time between two ``datetime`` or ``adatetime`` objects.
- """
- def __init__(self, start, end):
- """
- :param start: a ``datetime`` or ``adatetime`` object representing the
- start of the time span.
- :param end: a ``datetime`` or ``adatetime`` object representing the
- end of the time span.
- """
- if not isinstance(start, (datetime, adatetime)):
- raise TimeError("%r is not a datetime object" % start)
- if not isinstance(end, (datetime, adatetime)):
- raise TimeError("%r is not a datetime object" % end)
- self.start = copy.copy(start)
- self.end = copy.copy(end)
- def __eq__(self, other):
- if not other.__class__ is self.__class__:
- return False
- return self.start == other.start and self.end == other.end
- def __repr__(self):
- return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end)
- def disambiguated(self, basedate, debug=0):
- """Returns an unambiguous version of this object.
- >>> start = adatetime(year=2009, month=2)
- >>> end = adatetime(year=2009, month=10)
- >>> ts = timespan(start, end)
- >>> ts
- timespan(adatetime(2009, 2, None, None, None, None, None), adatetime(2009, 10, None, None, None, None, None))
- >>> td.disambiguated(datetime.now())
- timespan(datetime(2009, 2, 28, 0, 0, 0, 0), datetime(2009, 10, 31, 23, 59 ,59, 999999)
- """
- #- If year is in start but not end, use basedate.year for end
- #-- If year is in start but not end, but startdate is > basedate,
- # use "next <monthname>" to get end month/year
- #- If year is in end but not start, copy year from end to start
- #- Support "next february", "last april", etc.
- start, end = copy.copy(self.start), copy.copy(self.end)
- start_year_was_amb = start.year is None
- end_year_was_amb = end.year is None
- if has_no_date(start) and has_no_date(end):
- # The start and end points are just times, so use the basedate
- # for the date information.
- by, bm, bd = basedate.year, basedate.month, basedate.day
- start = start.replace(year=by, month=bm, day=bd)
- end = end.replace(year=by, month=bm, day=bd)
- else:
- # If one side has a year and the other doesn't, the decision
- # of what year to assign to the ambiguous side is kind of
- # arbitrary. I've used a heuristic here based on how the range
- # "reads", but it may only be reasonable in English. And maybe
- # even just to me.
- if start.year is None and end.year is None:
- # No year on either side, use the basedate
- start.year = end.year = basedate.year
- elif start.year is None:
- # No year in the start, use the year from the end
- start.year = end.year
- elif end.year is None:
- end.year = max(start.year, basedate.year)
- if start.year == end.year:
- # Once again, if one side has a month and day but the other side
- # doesn't, the disambiguation is arbitrary. Does "3 am to 5 am
- # tomorrow" mean 3 AM today to 5 AM tomorrow, or 3am tomorrow to
- # 5 am tomorrow? What I picked is similar to the year: if the
- # end has a month+day and the start doesn't, copy the month+day
- # from the end to the start UNLESS that would make the end come
- # before the start on that day, in which case use the basedate
- # instead. If the start has a month+day and the end doesn't, use
- # the basedate.
- start_dm = not (start.month is None and start.day is None)
- end_dm = not (end.month is None and end.day is None)
- if end_dm and not start_dm:
- if start.floor().time() > end.ceil().time():
- start.month = basedate.month
- start.day = basedate.day
- else:
- start.month = end.month
- start.day = end.day
- elif start_dm and not end_dm:
- end.month = basedate.month
- end.day = basedate.day
- if floor(start).date() > ceil(end).date():
- # If the disambiguated dates are out of order:
- # - If no start year was given, reduce the start year to put the
- # start before the end
- # - If no end year was given, increase the end year to put the end
- # after the start
- # - If a year was specified for both, just swap the start and end
- if start_year_was_amb:
- start.year = end.year - 1
- elif end_year_was_amb:
- end.year = start.year + 1
- else:
- start, end = end, start
- start = floor(start)
- end = ceil(end)
- if start.date() == end.date() and start.time() > end.time():
- # If the start and end are on the same day, but the start time
- # is after the end time, move the end time to the next day
- end += timedelta(days=1)
- return timespan(start, end)
- # Functions for working with datetime/adatetime objects
- def floor(at):
- if isinstance(at, datetime):
- return at
- return at.floor()
- def ceil(at):
- if isinstance(at, datetime):
- return at
- return at.ceil()
- def fill_in(at, basedate, units=adatetime.units):
- """Returns a copy of ``at`` with any unspecified (None) units filled in
- with values from ``basedate``.
- """
- if isinstance(at, datetime):
- return at
- args = {}
- for unit in units:
- v = getattr(at, unit)
- if v is None:
- v = getattr(basedate, unit)
- args[unit] = v
- return fix(adatetime(**args))
- def has_no_date(at):
- """Returns True if the given object is an ``adatetime`` where ``year``,
- ``month``, and ``day`` are all None.
- """
- if isinstance(at, datetime):
- return False
- return at.year is None and at.month is None and at.day is None
- def has_no_time(at):
- """Returns True if the given object is an ``adatetime`` where ``hour``,
- ``minute``, ``second`` and ``microsecond`` are all None.
- """
- if isinstance(at, datetime):
- return False
- return (at.hour is None and at.minute is None and at.second is None
- and at.microsecond is None)
- def is_ambiguous(at):
- """Returns True if the given object is an ``adatetime`` with any of its
- attributes equal to None.
- """
- if isinstance(at, datetime):
- return False
- return any((getattr(at, attr) is None) for attr in adatetime.units)
- def is_void(at):
- """Returns True if the given object is an ``adatetime`` with all of its
- attributes equal to None.
- """
- if isinstance(at, datetime):
- return False
- return all((getattr(at, attr) is None) for attr in adatetime.units)
- def fix(at):
- """If the given object is an ``adatetime`` that is unambiguous (because
- all its attributes are specified, that is, not equal to None), returns a
- ``datetime`` version of it. Otherwise returns the ``adatetime`` object
- unchanged.
- """
- if is_ambiguous(at) or isinstance(at, datetime):
- return at
- return datetime(year=at.year, month=at.month, day=at.day, hour=at.hour,
- minute=at.minute, second=at.second,
- microsecond=at.microsecond)
|