microproduct/atmosphericDelay/ISCEApp/site-packages/whoosh/util/times.py

468 lines
16 KiB
Python

# 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)