Source

whoosh / src / whoosh / support / times.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
#===============================================================================
# Copyright 2010 Matt Chaput
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#    http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#===============================================================================

import calendar, copy
from datetime import date, time, datetime, timedelta

from whoosh.support.relativedelta import relativedelta


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 datetime_to_long(dt):
        """Converts a datetime object to a long integer representing the number
        of microseconds since ``datetime.min``.
        """
        
        td = dt - dt.min
        total = td.days * 86400000000 # Microseconds in a day
        total += td.seconds * 1000000 # Microseconds in a second
        total += td.microseconds
        return total


# 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):
            self.year, self.month, self.day = year.year, year.month, year.day
            self.hour, self.minute, self.second = year.hour, year.minute, year.second
            self.microsecond = year.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 kwargs.iteritems():
            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)
        """
        
        year, month, day, hour, minute, second, microsecond =\
        self.year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond
        
        if year is None:
            raise ValueError("Date has no year")
        
        if month is None: month = 1
        if day is None: day = 1
        if hour is None: hour = 0
        if minute is None: minute = 0
        if second is None: second = 0
        if microsecond is None: microsecond = 0
        return datetime(year, month, day, hour, minute, second, microsecond)
    
    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)
        """
        
        year, month, day, hour, minute, second, microsecond =\
        self.year, self.month, self.day, self.hour, self.minute, self.second, self.microsecond
        
        if year is None:
            raise ValueError("Date has no year")
        
        if month is None: month = 12
        if day is None: day = calendar.monthrange(year, month)[1]
        if hour is None: hour = 23
        if minute is None: minute = 59
        if second is None: second = 59
        if microsecond is None: microsecond = 999999
        return datetime(year, month, day, hour, minute, second, microsecond)
    
    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.datetime(2009, 10, 31, 0, 0, 0, 0), datetime.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.datetime(2009, 2, 28, 0, 0, 0, 0), datetime.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)