~divmod-dev/divmod.org/trunk

« back to all changes in this revision

Viewing changes to Epsilon/epsilon/extime.py

  • Committer: Jean-Paul Calderone
  • Date: 2014-06-29 20:33:04 UTC
  • mfrom: (2749.1.1 remove-epsilon-1325289)
  • Revision ID: exarkun@twistedmatrix.com-20140629203304-gdkmbwl1suei4m97
mergeĀ lp:~exarkun/divmod.org/remove-epsilon-1325289

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: epsilon.test.test_extime -*-
2
 
"""
3
 
Extended date/time formatting and miscellaneous functionality.
4
 
 
5
 
See the class 'Time' for details.
6
 
"""
7
 
 
8
 
import datetime
9
 
import re
10
 
 
11
 
from email.Utils import parsedate_tz
12
 
 
13
 
_EPOCH = datetime.datetime.utcfromtimestamp(0)
14
 
 
15
 
 
16
 
class InvalidPrecision(Exception):
17
 
    """
18
 
    L{Time.asHumanly} was passed an invalid precision value.
19
 
    """
20
 
 
21
 
 
22
 
 
23
 
def sanitizeStructTime(struct):
24
 
    """
25
 
    Convert struct_time tuples with possibly invalid values to valid
26
 
    ones by substituting the closest valid value.
27
 
    """
28
 
    maxValues = (9999, 12, 31, 23, 59, 59)
29
 
    minValues = (1, 1, 1, 0, 0, 0)
30
 
    newstruct = []
31
 
    for value, maxValue, minValue in zip(struct[:6], maxValues, minValues):
32
 
        newstruct.append(max(minValue, min(value, maxValue)))
33
 
    return tuple(newstruct) + struct[6:]
34
 
 
35
 
def _timedeltaToSignHrMin(offset):
36
 
    """
37
 
    Return a (sign, hour, minute) triple for the offset described by timedelta.
38
 
 
39
 
    sign is a string, either "+" or "-". In the case of 0 offset, sign is "+".
40
 
    """
41
 
    minutes = round((offset.days * 3600000000 * 24
42
 
                     + offset.seconds * 1000000
43
 
                     + offset.microseconds)
44
 
                    / 60000000.0)
45
 
    if minutes < 0:
46
 
        sign = '-'
47
 
        minutes = -minutes
48
 
    else:
49
 
        sign = '+'
50
 
    return (sign, minutes // 60, minutes % 60)
51
 
 
52
 
def _timedeltaToSeconds(offset):
53
 
    """
54
 
    Convert a datetime.timedelta instance to simply a number of seconds.
55
 
 
56
 
    For example, you can specify purely second intervals with timedelta's
57
 
    constructor:
58
 
 
59
 
        >>> td = datetime.timedelta(seconds=99999999)
60
 
 
61
 
    but then you can't get them out again:
62
 
 
63
 
        >>> td.seconds
64
 
        35199
65
 
 
66
 
    This allows you to:
67
 
 
68
 
        >>> import epsilon.extime
69
 
        >>> epsilon.extime._timedeltaToSeconds(td)
70
 
        99999999.0
71
 
 
72
 
    @param offset: a L{datetime.timedelta} representing an interval that we
73
 
    wish to know the total number of seconds for.
74
 
 
75
 
    @return: a number of seconds
76
 
    @rtype: float
77
 
    """
78
 
    return ((offset.days * 60*60*24) +
79
 
            (offset.seconds) +
80
 
            (offset.microseconds * 1e-6))
81
 
 
82
 
class FixedOffset(datetime.tzinfo):
83
 
    _zeroOffset = datetime.timedelta()
84
 
 
85
 
    def __init__(self, hours, minutes):
86
 
        self.offset = datetime.timedelta(minutes = hours * 60 + minutes)
87
 
 
88
 
    def utcoffset(self, dt):
89
 
        return self.offset
90
 
 
91
 
    def tzname(self, dt):
92
 
        return _timedeltaToSignHrMin(self.offset)
93
 
 
94
 
    def dst(self, tz):
95
 
        return self._zeroOffset
96
 
 
97
 
    def __repr__(self):
98
 
        return '<%s.%s object at 0x%x offset %r>' % (
99
 
            self.__module__, type(self).__name__, id(self), self.offset)
100
 
 
101
 
 
102
 
 
103
 
class Time(object):
104
 
    """An object representing a well defined instant in time.
105
 
 
106
 
    A Time object unambiguously addresses some time, independent of timezones,
107
 
    contorted base-60 counting schemes, leap seconds, and the effects of
108
 
    general relativity. It provides methods for returning a representation of
109
 
    this time in various ways that a human or a programmer might find more
110
 
    useful in various applications.
111
 
 
112
 
    Every Time instance has an attribute 'resolution'. This can be ignored, or
113
 
    the instance can be considered to address a span of time. This resolution
114
 
    is determined by the value used to initalize the instance, or the
115
 
    resolution of the internal representation, whichever is greater. It is
116
 
    mostly useful when using input formats that allow the specification of
117
 
    whole days or weeks. For example, ISO 8601 allows one to state a time as,
118
 
    "2005-W03", meaning "the third week of 2005". In this case the resolution
119
 
    is set to one week. Other formats are considered to express only an instant
120
 
    in time, such as a POSIX timestamp, because the resolution of the time is
121
 
    limited only by the hardware's representation of a real number.
122
 
 
123
 
    Timezones are significant only for instances with a resolution greater than
124
 
    one day. When the timezone is insignificant, the result of methods like
125
 
    asISO8601TimeAndDate is the same for any given tzinfo parameter. Sort order
126
 
    is determined by the start of the period in UTC. For example, "today" sorts
127
 
    after "midnight today, central Europe", and before "midnight today, US
128
 
    Eastern". For applications that need to store a mix of timezone dependent
129
 
    and independent instances, it may be wise to store them separately, since
130
 
    the time between the start and end of today in the local timezone may not
131
 
    include the start of today in UTC, and thus not independent instances
132
 
    addressing the whole day. In other words, the desired sort order (the one
133
 
    where just "Monday" sorts before any more precise time in "Monday", and
134
 
    after any in "Sunday") of Time instances is dependant on the timezone
135
 
    context.
136
 
 
137
 
    Date arithmetic and boolean operations operate on instants in time, not
138
 
    periods. In this case, the start of the period is used as the value, and
139
 
    the result has a resolution of 0.
140
 
 
141
 
    For containment tests with the 'in' operator, the period addressed by the
142
 
    instance is used.
143
 
 
144
 
    The methods beginning with 'from' are constructors to create instances from
145
 
    various formats. Some of them are textual formats, and others are other
146
 
    time types commonly found in Python code.
147
 
 
148
 
    Likewise, methods beginning with 'as' return the represented time in
149
 
    various formats. Some of these methods should try to reflect the resolution
150
 
    of the instance. However, they don't yet.
151
 
 
152
 
    For formats with both a constructor and a formatter, d == fromFu(d.asFu())
153
 
 
154
 
    @type resolution: datetime.timedelta
155
 
    @ivar resolution: the length of the period to which this instance could
156
 
    refer. For example, "Today, 13:38" could refer to any time between 13:38
157
 
    until but not including 13:39. In this case resolution would be
158
 
    timedelta(minutes=1).
159
 
    """
160
 
 
161
 
    # the instance variable _time is the internal representation of time. It
162
 
    # is a naive datetime object which is always UTC. A UTC tzinfo would be
163
 
    # great, if one existed, and anyway it complicates pickling.
164
 
 
165
 
 
166
 
    class Precision(object):
167
 
        MINUTES = object() 
168
 
        SECONDS = object()
169
 
 
170
 
 
171
 
    _timeFormat = {
172
 
            Precision.MINUTES: '%I:%M %p',
173
 
            Precision.SECONDS: '%I:%M:%S %p'}
174
 
 
175
 
    rfc2822Weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
176
 
 
177
 
    rfc2822Months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug',
178
 
                     'Sep', 'Oct', 'Nov', 'Dec']
179
 
 
180
 
    resolution = datetime.timedelta.resolution
181
 
 
182
 
    #
183
 
    # Methods to create new instances
184
 
    #
185
 
 
186
 
    def __init__(self):
187
 
        """Return a new Time instance representing the time now.
188
 
 
189
 
        See also the fromFu methods to create new instances from other types of
190
 
        initializers.
191
 
        """
192
 
        self._time = datetime.datetime.utcnow()
193
 
 
194
 
 
195
 
    def _fromWeekday(klass, match, tzinfo, now):
196
 
        weekday = klass.weekdays.index(match.group('weekday').lower())
197
 
        dtnow = now.asDatetime().replace(
198
 
            hour=0, minute=0, second=0, microsecond=0)
199
 
        daysInFuture = (weekday - dtnow.weekday()) % len(klass.weekdays)
200
 
        if daysInFuture == 0:
201
 
            daysInFuture = 7
202
 
        self = klass.fromDatetime(dtnow + datetime.timedelta(days=daysInFuture))
203
 
        assert self.asDatetime().weekday() == weekday
204
 
        self.resolution = datetime.timedelta(days=1)
205
 
        return self
206
 
 
207
 
 
208
 
    def _fromTodayOrTomorrow(klass, match, tzinfo, now):
209
 
        dtnow = now.asDatetime().replace(
210
 
            hour=0, minute=0, second=0, microsecond=0)
211
 
        when = match.group(0).lower()
212
 
        if when == 'tomorrow':
213
 
            dtnow += datetime.timedelta(days=1)
214
 
        elif when == 'yesterday':
215
 
            dtnow -= datetime.timedelta(days=1)
216
 
        else:
217
 
            assert when == 'today'
218
 
        self = klass.fromDatetime(dtnow)
219
 
        self.resolution = datetime.timedelta(days=1)
220
 
        return self
221
 
 
222
 
 
223
 
    def _fromTime(klass, match, tzinfo, now):
224
 
        minute = int(match.group('minute'))
225
 
        hour = int(match.group('hour'))
226
 
        ampm = (match.group('ampm') or '').lower()
227
 
        if ampm:
228
 
            if not 1 <= hour <= 12:
229
 
                raise ValueError, 'hour %i is not in 1..12' % (hour,)
230
 
            if hour == 12 and ampm == 'am':
231
 
                hour = 0
232
 
            elif ampm == 'pm':
233
 
                hour += 12
234
 
        if not 0 <= hour <= 23:
235
 
            raise ValueError, 'hour %i is not in 0..23' % (hour,)
236
 
 
237
 
        dtnow = now.asDatetime(tzinfo).replace(second=0, microsecond=0)
238
 
        dtthen = dtnow.replace(hour=hour, minute=minute)
239
 
        if dtthen < dtnow:
240
 
            dtthen += datetime.timedelta(days=1)
241
 
 
242
 
        self = klass.fromDatetime(dtthen)
243
 
        self.resolution = datetime.timedelta(minutes=1)
244
 
        return self
245
 
 
246
 
 
247
 
    def _fromNoonOrMidnight(klass, match, tzinfo, now):
248
 
        when = match.group(0).lower()
249
 
        if when == 'noon':
250
 
            hour = 12
251
 
        else:
252
 
            assert when == 'midnight'
253
 
            hour = 0
254
 
        dtnow = now.asDatetime(tzinfo).replace(
255
 
            minute=0, second=0, microsecond=0)
256
 
        dtthen = dtnow.replace(hour=hour)
257
 
        if dtthen < dtnow:
258
 
            dtthen += datetime.timedelta(days=1)
259
 
 
260
 
        self = klass.fromDatetime(dtthen)
261
 
        self.resolution = datetime.timedelta(minutes=1)
262
 
        return self
263
 
 
264
 
    def _fromNow(klass, match, tzinfo, now):
265
 
        # coerce our 'now' argument to an instant
266
 
        return now + datetime.timedelta(0)
267
 
 
268
 
    weekdays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday',
269
 
                'saturday', 'sunday']
270
 
 
271
 
    humanlyPatterns = [
272
 
        (re.compile(r"""
273
 
            \b
274
 
            ((next|this)\s+)?
275
 
            (?P<weekday>
276
 
                monday
277
 
                | tuesday
278
 
                | wednesday
279
 
                | thursday
280
 
                | friday
281
 
                | saturday
282
 
                | sunday
283
 
            )
284
 
            \b
285
 
            """, re.IGNORECASE | re.VERBOSE),
286
 
            _fromWeekday),
287
 
        (re.compile(r"\b(today|tomorrow|yesterday)\b", re.IGNORECASE),
288
 
            _fromTodayOrTomorrow),
289
 
        (re.compile(r"""
290
 
            \b
291
 
            (?P<hour>\d{1,2}):(?P<minute>\d{2})
292
 
            (\s*(?P<ampm>am|pm))?
293
 
            \b
294
 
            """, re.IGNORECASE | re.VERBOSE),
295
 
            _fromTime),
296
 
        (re.compile(r"\b(noon|midnight)\b", re.IGNORECASE),
297
 
            _fromNoonOrMidnight),
298
 
        (re.compile(r"\b(now)\b", re.IGNORECASE),
299
 
            _fromNow),
300
 
    ]
301
 
 
302
 
    _fromWeekday = classmethod(_fromWeekday)
303
 
    _fromTodayOrTomorrow = classmethod(_fromTodayOrTomorrow)
304
 
    _fromTime = classmethod(_fromTime)
305
 
    _fromNoonOrMidnight = classmethod(_fromNoonOrMidnight)
306
 
    _fromNow = classmethod(_fromNow)
307
 
 
308
 
 
309
 
    def fromHumanly(klass, humanStr, tzinfo=None, now=None):
310
 
        """Return a new Time instance from a string a human might type.
311
 
 
312
 
        @param humanStr: the string to be parsed.
313
 
 
314
 
        @param tzinfo: A tzinfo instance indicating the timezone to assume if
315
 
        none is specified in humanStr. If None, assume UTC.
316
 
 
317
 
        @param now: A Time instance to be considered "now" for when
318
 
        interpreting relative dates like "tomorrow". If None, use the real now.
319
 
 
320
 
        Total crap now, it just supports weekdays, "today" and "tomorrow" for
321
 
        now. This is pretty insufficient and useless, but good enough for some
322
 
        demo functionality, or something.
323
 
        """
324
 
        humanStr = humanStr.strip()
325
 
        if now is None:
326
 
            now = Time()
327
 
        if tzinfo is None:
328
 
            tzinfo = FixedOffset(0, 0)
329
 
 
330
 
        for pattern, creator in klass.humanlyPatterns:
331
 
            match = pattern.match(humanStr)
332
 
            if not match \
333
 
            or match.span()[1] != len(humanStr):
334
 
                continue
335
 
            try:
336
 
                return creator(klass, match, tzinfo, now)
337
 
            except ValueError:
338
 
                continue
339
 
        raise ValueError, 'could not parse date: %r' % (humanStr,)
340
 
 
341
 
    fromHumanly = classmethod(fromHumanly)
342
 
 
343
 
 
344
 
    iso8601pattern = re.compile(r"""
345
 
        ^ (?P<year> \d{4})
346
 
        (
347
 
            # a year may optionally be followed by one of:
348
 
            # - a month
349
 
            # - a week
350
 
            # - a specific day, and an optional time
351
 
            #     a specific day is one of:
352
 
            #     - a month and day
353
 
            #     - week and weekday
354
 
            #     - a day of the year
355
 
            (
356
 
                -? (?P<month1> \d{2})
357
 
                |
358
 
                -? W (?P<week1> \d{2})
359
 
                |
360
 
                (
361
 
                    -? (?P<month2> \d{2})
362
 
                    -? (?P<day> \d{2})
363
 
                    |
364
 
                    -? W (?P<week2> \d{2})
365
 
                    -? (?P<weekday> \d)
366
 
                    |
367
 
                    -? (?P<dayofyear> \d{3})
368
 
                )
369
 
                (
370
 
                    T (?P<hour> \d{2})
371
 
                    (
372
 
                        :? (?P<minute> \d{2})
373
 
                        (
374
 
                            :? (?P<second> \d{2})
375
 
                            (
376
 
                                [\.,] (?P<fractionalsec> \d+)
377
 
                            )?
378
 
                        )?
379
 
                    )?
380
 
                    (
381
 
                        (?P<zulu> Z)
382
 
                        |
383
 
                        (?P<tzhour> [+\-]\d{2})
384
 
                        (
385
 
                            :? (?P<tzmin> \d{2})
386
 
                        )?
387
 
                    )?
388
 
                )?
389
 
            )?
390
 
        )? $""", re.VERBOSE)
391
 
 
392
 
 
393
 
    def fromISO8601TimeAndDate(klass, iso8601string, tzinfo=None):
394
 
        """Return a new Time instance from a string formated as in ISO 8601.
395
 
 
396
 
        If the given string contains no timezone, it is assumed to be in the
397
 
        timezone specified by the parameter `tzinfo`, or UTC if tzinfo is None.
398
 
        An input string with an explicit timezone will always override tzinfo.
399
 
 
400
 
        If the given iso8601string does not contain all parts of the time, they
401
 
        will default to 0 in the timezone given by `tzinfo`.
402
 
 
403
 
        WARNING: this function is incomplete. ISO is dumb and their standards
404
 
        are not free. Only a subset of all valid ISO 8601 dates are parsed,
405
 
        because I can't find a formal description of the format. However,
406
 
        common ones should work.
407
 
        """
408
 
 
409
 
        def calculateTimezone():
410
 
            if groups['zulu'] == 'Z':
411
 
                return FixedOffset(0, 0)
412
 
            else:
413
 
                tzhour = groups.pop('tzhour')
414
 
                tzmin = groups.pop('tzmin')
415
 
                if tzhour is not None:
416
 
                    return FixedOffset(int(tzhour), int(tzmin or 0))
417
 
            return tzinfo or FixedOffset(0, 0)
418
 
 
419
 
        def coerceGroups():
420
 
            groups['month'] = groups['month1'] or groups['month2']
421
 
            groups['week'] = groups['week1'] or groups['week2']
422
 
            # don't include fractional seconds, because it's not an integer.
423
 
            defaultTo0 = ['hour', 'minute', 'second']
424
 
            defaultTo1 = ['month', 'day', 'week', 'weekday', 'dayofyear']
425
 
            if groups['fractionalsec'] is None:
426
 
                groups['fractionalsec'] = '0'
427
 
            for key in defaultTo0:
428
 
                if groups[key] is None:
429
 
                    groups[key] = 0
430
 
            for key in defaultTo1:
431
 
                if groups[key] is None:
432
 
                    groups[key] = 1
433
 
            groups['fractionalsec'] = float('.'+groups['fractionalsec'])
434
 
            for key in defaultTo0 + defaultTo1 + ['year']:
435
 
                groups[key] = int(groups[key])
436
 
 
437
 
            for group, min, max in [
438
 
                # some years have only 52 weeks
439
 
                ('week', 1, 53),
440
 
                ('weekday', 1, 7),
441
 
                ('month', 1, 12),
442
 
                ('day', 1, 31),
443
 
                ('hour', 0, 24),
444
 
                ('minute', 0, 59),
445
 
 
446
 
                # Sometime in the 22nd century AD, two leap seconds will be
447
 
                # required every year.  In the 25th century AD, four every
448
 
                # year.  We'll ignore that for now though because it would be
449
 
                # tricky to get right and we certainly don't need it for our
450
 
                # target applications.  In other words, post-singularity
451
 
                # Martian users, please do not rely on this code for
452
 
                # compatibility with Greater Galactic Protectorate of Earth
453
 
                # date/time formatting!  Apologies, but no library I know of in
454
 
                # Python is sufficient for processing their dates and times
455
 
                # without ADA bindings to get the radiation-safety zone counter
456
 
                # correct. -glyph
457
 
 
458
 
                ('second', 0, 61),
459
 
                # don't forget leap years
460
 
                ('dayofyear', 1, 366)]:
461
 
                if not min <= groups[group] <= max:
462
 
                    raise ValueError, '%s must be in %i..%i' % (group, min, max)
463
 
 
464
 
        def determineResolution():
465
 
            if match.group('fractionalsec') is not None:
466
 
                return max(datetime.timedelta.resolution,
467
 
                    datetime.timedelta(
468
 
                        microseconds=1 * 10 ** -len(
469
 
                            match.group('fractionalsec')) * 1000000))
470
 
 
471
 
            for testGroup, resolution in [
472
 
            ('second', datetime.timedelta(seconds=1)),
473
 
            ('minute', datetime.timedelta(minutes=1)),
474
 
            ('hour', datetime.timedelta(hours=1)),
475
 
            ('weekday', datetime.timedelta(days=1)),
476
 
            ('dayofyear', datetime.timedelta(days=1)),
477
 
            ('day', datetime.timedelta(days=1)),
478
 
            ('week1', datetime.timedelta(weeks=1)),
479
 
            ('week2', datetime.timedelta(weeks=1))]:
480
 
                if match.group(testGroup) is not None:
481
 
                    return resolution
482
 
 
483
 
            if match.group('month1') is not None \
484
 
            or match.group('month2') is not None:
485
 
                if self._time.month == 12:
486
 
                    return datetime.timedelta(days=31)
487
 
                nextMonth = self._time.replace(month=self._time.month+1)
488
 
                return nextMonth - self._time
489
 
            else:
490
 
                nextYear = self._time.replace(year=self._time.year+1)
491
 
                return nextYear - self._time
492
 
 
493
 
        def calculateDtime(tzinfo):
494
 
            """Calculate a datetime for the start of the addressed period."""
495
 
 
496
 
            if match.group('week1') is not None \
497
 
            or match.group('week2') is not None:
498
 
                if not 0 < groups['week'] <= 53:
499
 
                    raise ValueError(
500
 
                        'week must be in 1..53 (was %i)' % (groups['week'],))
501
 
                dtime = datetime.datetime(
502
 
                    groups['year'],
503
 
                    1,
504
 
                    4,
505
 
                    groups['hour'],
506
 
                    groups['minute'],
507
 
                    groups['second'],
508
 
                    int(round(groups['fractionalsec'] * 1000000)),
509
 
                    tzinfo=tzinfo
510
 
                )
511
 
                dtime -= datetime.timedelta(days = dtime.weekday())
512
 
                dtime += datetime.timedelta(
513
 
                    days = (groups['week']-1) * 7 + groups['weekday'] - 1)
514
 
                if dtime.isocalendar() != (
515
 
                    groups['year'], groups['week'], groups['weekday']):
516
 
                    # actually the problem could be an error in my logic, but
517
 
                    # nothing should cause this but requesting week 53 of a
518
 
                    # year with 52 weeks.
519
 
                    raise ValueError('year %04i has no week %02i' %
520
 
                                     (groups['year'], groups['week']))
521
 
                return dtime
522
 
 
523
 
            if match.group('dayofyear') is not None:
524
 
                dtime = datetime.datetime(
525
 
                    groups['year'],
526
 
                    1,
527
 
                    1,
528
 
                    groups['hour'],
529
 
                    groups['minute'],
530
 
                    groups['second'],
531
 
                    int(round(groups['fractionalsec'] * 1000000)),
532
 
                    tzinfo=tzinfo
533
 
                )
534
 
                dtime += datetime.timedelta(days=groups['dayofyear']-1)
535
 
                if dtime.year != groups['year']:
536
 
                    raise ValueError(
537
 
                        'year %04i has no day of year %03i' %
538
 
                        (groups['year'], groups['dayofyear']))
539
 
                return dtime
540
 
 
541
 
            else:
542
 
                return datetime.datetime(
543
 
                    groups['year'],
544
 
                    groups['month'],
545
 
                    groups['day'],
546
 
                    groups['hour'],
547
 
                    groups['minute'],
548
 
                    groups['second'],
549
 
                    int(round(groups['fractionalsec'] * 1000000)),
550
 
                    tzinfo=tzinfo
551
 
                )
552
 
 
553
 
 
554
 
        match = klass.iso8601pattern.match(iso8601string)
555
 
        if match is None:
556
 
            raise ValueError(
557
 
                '%r could not be parsed as an ISO 8601 date and time' %
558
 
                (iso8601string,))
559
 
 
560
 
        groups = match.groupdict()
561
 
        coerceGroups()
562
 
        if match.group('hour') is not None:
563
 
            timezone = calculateTimezone()
564
 
        else:
565
 
            timezone = None
566
 
        self = klass.fromDatetime(calculateDtime(timezone))
567
 
        self.resolution = determineResolution()
568
 
        return self
569
 
 
570
 
    fromISO8601TimeAndDate = classmethod(fromISO8601TimeAndDate)
571
 
 
572
 
    def fromStructTime(klass, structTime, tzinfo=None):
573
 
        """Return a new Time instance from a time.struct_time.
574
 
 
575
 
        If tzinfo is None, structTime is in UTC. Otherwise, tzinfo is a
576
 
        datetime.tzinfo instance coresponding to the timezone in which
577
 
        structTime is.
578
 
 
579
 
        Many of the functions in the standard time module return these things.
580
 
        This will also work with a plain 9-tuple, for parity with the time
581
 
        module. The last three elements, or tm_wday, tm_yday, and tm_isdst are
582
 
        ignored.
583
 
        """
584
 
        dtime = datetime.datetime(tzinfo=tzinfo, *structTime[:6])
585
 
        self = klass.fromDatetime(dtime)
586
 
        self.resolution = datetime.timedelta(seconds=1)
587
 
        return self
588
 
 
589
 
    fromStructTime = classmethod(fromStructTime)
590
 
 
591
 
    def fromDatetime(klass, dtime):
592
 
        """Return a new Time instance from a datetime.datetime instance.
593
 
 
594
 
        If the datetime instance does not have an associated timezone, it is
595
 
        assumed to be UTC.
596
 
        """
597
 
        self = klass.__new__(klass)
598
 
        if dtime.tzinfo is not None:
599
 
            self._time = dtime.astimezone(FixedOffset(0, 0)).replace(tzinfo=None)
600
 
        else:
601
 
            self._time = dtime
602
 
        self.resolution = datetime.timedelta.resolution
603
 
        return self
604
 
 
605
 
    fromDatetime = classmethod(fromDatetime)
606
 
 
607
 
    def fromPOSIXTimestamp(klass, secs):
608
 
        """Return a new Time instance from seconds since the POSIX epoch.
609
 
 
610
 
        The POSIX epoch is midnight Jan 1, 1970 UTC. According to POSIX, leap
611
 
        seconds don't exist, so one UTC day is exactly 86400 seconds, even if
612
 
        it wasn't.
613
 
 
614
 
        @param secs: a number of seconds, represented as an integer, long or
615
 
        float.
616
 
        """
617
 
        self = klass.fromDatetime(_EPOCH + datetime.timedelta(seconds=secs))
618
 
        self.resolution = datetime.timedelta()
619
 
        return self
620
 
 
621
 
    fromPOSIXTimestamp = classmethod(fromPOSIXTimestamp)
622
 
 
623
 
    def fromRFC2822(klass, rfc822string):
624
 
        """
625
 
        Return a new Time instance from a string formated as described in RFC 2822.
626
 
 
627
 
        @type rfc822string: str
628
 
 
629
 
        @raise ValueError: if the timestamp is not formatted properly (or if
630
 
        certain obsoleted elements of the specification are used).
631
 
 
632
 
        @return: a new L{Time}
633
 
        """
634
 
 
635
 
        # parsedate_tz is going to give us a "struct_time plus", a 10-tuple
636
 
        # containing the 9 values a struct_time would, i.e.: (tm_year, tm_mon,
637
 
        # tm_day, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst), plus a
638
 
        # bonus "offset", which is an offset (in _seconds_, of all things).
639
 
 
640
 
        maybeStructTimePlus = parsedate_tz(rfc822string)
641
 
 
642
 
        if maybeStructTimePlus is None:
643
 
            raise ValueError, 'could not parse RFC 2822 date %r' % (rfc822string,)
644
 
        structTimePlus = sanitizeStructTime(maybeStructTimePlus)
645
 
        offsetInSeconds = structTimePlus[-1]
646
 
        if offsetInSeconds is None:
647
 
            offsetInSeconds = 0
648
 
        self = klass.fromStructTime(
649
 
            structTimePlus,
650
 
            FixedOffset(
651
 
                hours=0,
652
 
                minutes=offsetInSeconds // 60))
653
 
        self.resolution = datetime.timedelta(seconds=1)
654
 
        return self
655
 
 
656
 
    fromRFC2822 = classmethod(fromRFC2822)
657
 
 
658
 
    #
659
 
    # Methods to produce various formats
660
 
    #
661
 
 
662
 
    def asPOSIXTimestamp(self):
663
 
        """Return this time as a timestamp as specified by POSIX.
664
 
 
665
 
        This timestamp is the count of the number of seconds since Midnight,
666
 
        Jan 1 1970 UTC, ignoring leap seconds.
667
 
        """
668
 
        mytimedelta = self._time - _EPOCH
669
 
        return _timedeltaToSeconds(mytimedelta)
670
 
 
671
 
    def asDatetime(self, tzinfo=None):
672
 
        """Return this time as an aware datetime.datetime instance.
673
 
 
674
 
        The returned datetime object has the specified tzinfo, or a tzinfo
675
 
        describing UTC if the tzinfo parameter is None.
676
 
        """
677
 
        if tzinfo is None:
678
 
            tzinfo = FixedOffset(0, 0)
679
 
 
680
 
        if not self.isTimezoneDependent():
681
 
            return self._time.replace(tzinfo=tzinfo)
682
 
        else:
683
 
            return self._time.replace(tzinfo=FixedOffset(0, 0)).astimezone(tzinfo)
684
 
 
685
 
    def asNaiveDatetime(self, tzinfo=None):
686
 
        """Return this time as a naive datetime.datetime instance.
687
 
 
688
 
        The returned datetime object has its tzinfo set to None, but is in the
689
 
        timezone given by the tzinfo parameter, or UTC if the parameter is
690
 
        None.
691
 
        """
692
 
        return self.asDatetime(tzinfo).replace(tzinfo=None)
693
 
 
694
 
    def asRFC2822(self, tzinfo=None, includeDayOfWeek=True):
695
 
        """Return this Time formatted as specified in RFC 2822.
696
 
 
697
 
        RFC 2822 specifies the format of email messages.
698
 
 
699
 
        RFC 2822 says times in email addresses should reflect the local
700
 
        timezone. If tzinfo is a datetime.tzinfo instance, the returned
701
 
        formatted string will reflect that timezone. Otherwise, the timezone
702
 
        will be '-0000', which RFC 2822 defines as UTC, but with an unknown
703
 
        local timezone.
704
 
 
705
 
        RFC 2822 states that the weekday is optional. The parameter
706
 
        includeDayOfWeek indicates whether or not to include it.
707
 
        """
708
 
        dtime = self.asDatetime(tzinfo)
709
 
 
710
 
        if tzinfo is None:
711
 
            rfcoffset = '-0000'
712
 
        else:
713
 
            rfcoffset = '%s%02i%02i' % _timedeltaToSignHrMin(dtime.utcoffset())
714
 
 
715
 
        rfcstring = ''
716
 
        if includeDayOfWeek:
717
 
            rfcstring += self.rfc2822Weekdays[dtime.weekday()] + ', '
718
 
 
719
 
        rfcstring += '%i %s %4i %02i:%02i:%02i %s' % (
720
 
            dtime.day,
721
 
            self.rfc2822Months[dtime.month - 1],
722
 
            dtime.year,
723
 
            dtime.hour,
724
 
            dtime.minute,
725
 
            dtime.second,
726
 
            rfcoffset)
727
 
 
728
 
        return rfcstring
729
 
 
730
 
    def asISO8601TimeAndDate(self, includeDelimiters=True, tzinfo=None,
731
 
                             includeTimezone=True):
732
 
        """Return this time formatted as specified by ISO 8861.
733
 
 
734
 
        ISO 8601 allows optional dashes to delimit dates and colons to delimit
735
 
        times. The parameter includeDelimiters (default True) defines the
736
 
        inclusion of these delimiters in the output.
737
 
 
738
 
        If tzinfo is a datetime.tzinfo instance, the output time will be in the
739
 
        timezone given. If it is None (the default), then the timezone string
740
 
        will not be included in the output, and the time will be in UTC.
741
 
 
742
 
        The includeTimezone parameter coresponds to the inclusion of an
743
 
        explicit timezone. The default is True.
744
 
        """
745
 
        if not self.isTimezoneDependent():
746
 
            tzinfo = None
747
 
        dtime = self.asDatetime(tzinfo)
748
 
 
749
 
        if includeDelimiters:
750
 
            dateSep = '-'
751
 
            timeSep = ':'
752
 
        else:
753
 
            dateSep = timeSep = ''
754
 
 
755
 
        if includeTimezone:
756
 
            if tzinfo is None:
757
 
                timezone = '+00%s00' % (timeSep,)
758
 
            else:
759
 
                sign, hour, min = _timedeltaToSignHrMin(dtime.utcoffset())
760
 
                timezone = '%s%02i%s%02i' % (sign, hour, timeSep, min)
761
 
        else:
762
 
            timezone = ''
763
 
 
764
 
        microsecond = ('%06i' % (dtime.microsecond,)).rstrip('0')
765
 
        if microsecond:
766
 
            microsecond = '.' + microsecond
767
 
 
768
 
        parts = [
769
 
            ('%04i' % (dtime.year,), datetime.timedelta(days=366)),
770
 
            ('%s%02i' % (dateSep, dtime.month), datetime.timedelta(days=31)),
771
 
            ('%s%02i' % (dateSep, dtime.day), datetime.timedelta(days=1)),
772
 
            ('T', datetime.timedelta(hours=1)),
773
 
            ('%02i' % (dtime.hour,), datetime.timedelta(hours=1)),
774
 
            ('%s%02i' % (timeSep, dtime.minute), datetime.timedelta(minutes=1)),
775
 
            ('%s%02i' % (timeSep, dtime.second), datetime.timedelta(seconds=1)),
776
 
            (microsecond, datetime.timedelta(microseconds=1)),
777
 
            (timezone, datetime.timedelta(hours=1))
778
 
        ]
779
 
 
780
 
        formatted = ''
781
 
        for part, minResolution in parts:
782
 
            if self.resolution <= minResolution:
783
 
                formatted += part
784
 
 
785
 
        return formatted
786
 
 
787
 
    def asStructTime(self, tzinfo=None):
788
 
        """Return this time represented as a time.struct_time.
789
 
 
790
 
        tzinfo is a datetime.tzinfo instance coresponding to the desired
791
 
        timezone of the output. If is is the default None, UTC is assumed.
792
 
        """
793
 
        dtime = self.asDatetime(tzinfo)
794
 
        if tzinfo is None:
795
 
            return dtime.utctimetuple()
796
 
        else:
797
 
            return dtime.timetuple()
798
 
 
799
 
    def asHumanly(self, tzinfo=None, now=None, precision=Precision.MINUTES):
800
 
        """Return this time as a short string, tailored to the current time.
801
 
 
802
 
        Parts of the date that can be assumed are omitted. Consequently, the
803
 
        output string depends on the current time. This is the format used for
804
 
        displaying dates in most user visible places in the quotient web UI.
805
 
 
806
 
        By default, the current time is determined by the system clock. The
807
 
        current time used for formatting the time can be changed by providing a
808
 
        Time instance as the parameter 'now'.
809
 
 
810
 
        @param precision: The smallest unit of time that will be represented
811
 
        in the returned string.  Valid values are L{Time.Precision.MINUTES} and
812
 
        L{Time.Precision.SECONDS}.
813
 
 
814
 
        @raise InvalidPrecision: if the specified precision is not either
815
 
        L{Time.Precision.MINUTES} or L{Time.Precision.SECONDS}.
816
 
        """
817
 
        try:
818
 
            timeFormat = Time._timeFormat[precision]
819
 
        except KeyError:
820
 
            raise InvalidPrecision(
821
 
                    'Use Time.Precision.MINUTES or Time.Precision.SECONDS')
822
 
 
823
 
        if now is None:
824
 
            now = Time().asDatetime(tzinfo)
825
 
        else:
826
 
            now = now.asDatetime(tzinfo)
827
 
        dtime = self.asDatetime(tzinfo)
828
 
 
829
 
        # Same day?
830
 
        if dtime.date() == now.date():
831
 
            if self.isAllDay():
832
 
                return 'all day'
833
 
            return dtime.strftime(timeFormat).lower()
834
 
        else:
835
 
            res = str(dtime.date().day) + dtime.strftime(' %b')  # day + month
836
 
            # Different year?
837
 
            if not dtime.date().year == now.date().year:
838
 
                res += dtime.strftime(' %Y')
839
 
            if not self.isAllDay():
840
 
                res += dtime.strftime(', %s' % (timeFormat,)).lower()
841
 
            return res
842
 
 
843
 
    #
844
 
    # methods to return related times
845
 
    #
846
 
 
847
 
    def getBounds(self, tzinfo=None):
848
 
        """
849
 
        Return a pair describing the bounds of self.
850
 
 
851
 
        This returns a pair (min, max) of Time instances. It is not quite the
852
 
        same as (self, self + self.resolution). This is because timezones are
853
 
        insignificant for instances with a resolution greater or equal to 1
854
 
        day.
855
 
 
856
 
        To illustrate the problem, consider a Time instance::
857
 
 
858
 
            T = Time.fromHumanly('today', tzinfo=anything)
859
 
 
860
 
        This will return an equivalent instance independent of the tzinfo used.
861
 
        The hour, minute, and second of this instance are 0, and its resolution
862
 
        is one day.
863
 
 
864
 
        Now say we have a sorted list of times, and we want to get all times
865
 
        for 'today', where whoever said 'today' is in a timezone that's 5 hours
866
 
        ahead of UTC. The start of 'today' in this timezone is UTC 05:00. The
867
 
        example instance T above is before this, but obviously it is today.
868
 
 
869
 
        The min and max times this returns are such that all potentially
870
 
        matching instances are within this range. However, this range might
871
 
        contain unmatching instances.
872
 
 
873
 
        As an example of this, if 'today' is April first 2005, then
874
 
        Time.fromISO8601TimeAndDate('2005-04-01T00:00:00') sorts in the same
875
 
        place as T from above, but is not in the UTC+5 'today'.
876
 
 
877
 
        TIME IS FUN!
878
 
        """
879
 
        if self.resolution >= datetime.timedelta(days=1) \
880
 
        and tzinfo is not None:
881
 
            time = self._time.replace(tzinfo=tzinfo)
882
 
        else:
883
 
            time = self._time
884
 
 
885
 
        return (
886
 
            min(self.fromDatetime(time), self.fromDatetime(self._time)),
887
 
            max(self.fromDatetime(time + self.resolution),
888
 
                self.fromDatetime(self._time + self.resolution))
889
 
        )
890
 
 
891
 
    def oneDay(self):
892
 
        """Return a Time instance representing the day of the start of self.
893
 
 
894
 
        The returned new instance will be set to midnight of the day containing
895
 
        the first instant of self in the specified timezone, and have a
896
 
        resolution of datetime.timedelta(days=1).
897
 
        """
898
 
        day = self.__class__.fromDatetime(self.asDatetime().replace(
899
 
                hour=0, minute=0, second=0, microsecond=0))
900
 
        day.resolution = datetime.timedelta(days=1)
901
 
        return day
902
 
 
903
 
    #
904
 
    # useful predicates
905
 
    #
906
 
 
907
 
    def isAllDay(self):
908
 
        """Return True iff this instance represents exactly all day."""
909
 
        return self.resolution == datetime.timedelta(days=1)
910
 
 
911
 
    def isTimezoneDependent(self):
912
 
        """Return True iff timezone is relevant for this instance.
913
 
 
914
 
        Timezone is only relevent for instances with a resolution better than
915
 
        one day.
916
 
        """
917
 
        return self.resolution < datetime.timedelta(days=1)
918
 
 
919
 
    #
920
 
    # other magic methods
921
 
    #
922
 
 
923
 
    def __cmp__(self, other):
924
 
        if not isinstance(other, Time):
925
 
            raise TypeError("Cannot meaningfully compare %r with %r" % (self, other))
926
 
        return cmp(self._time, other._time)
927
 
 
928
 
    def __eq__(self, other):
929
 
        if isinstance(other, Time):
930
 
            return cmp(self._time, other._time) == 0
931
 
        return False
932
 
 
933
 
    def __ne__(self, other):
934
 
        return not (self == other)
935
 
 
936
 
    def __repr__(self):
937
 
        return 'extime.Time.fromDatetime(%r)' % (self._time,)
938
 
 
939
 
    __str__ = asISO8601TimeAndDate
940
 
 
941
 
    def __contains__(self, other):
942
 
        """Test if another Time instance is entirely within the period addressed by this one."""
943
 
        if not isinstance(other, Time):
944
 
            raise TypeError(
945
 
                '%r is not a Time instance; can not test for containment'
946
 
                % (other,))
947
 
        if other._time < self._time:
948
 
            return False
949
 
        if self._time + self.resolution < other._time + other.resolution:
950
 
            return False
951
 
        return True
952
 
 
953
 
    def __add__(self, addend):
954
 
        if not isinstance(addend, datetime.timedelta):
955
 
            raise TypeError, 'expected a datetime.timedelta instance'
956
 
        return Time.fromDatetime(self._time + addend)
957
 
 
958
 
    def __sub__(self, subtrahend):
959
 
        """
960
 
        Implement subtraction of an interval or another time from this one.
961
 
 
962
 
        @type subtrahend: L{datetime.timedelta} or L{Time}
963
 
 
964
 
        @param subtrahend: The object to be subtracted from this one.
965
 
 
966
 
        @rtype: L{datetime.timedelta} or L{Time}
967
 
 
968
 
        @return: If C{subtrahend} is a L{datetime.timedelta}, the result is
969
 
        a L{Time} instance which is offset from this one by that amount.  If
970
 
        C{subtrahend} is a L{Time}, the result is a L{datetime.timedelta}
971
 
        instance which gives the difference between it and this L{Time}
972
 
        instance.
973
 
        """
974
 
        if isinstance(subtrahend, datetime.timedelta):
975
 
            return Time.fromDatetime(self._time - subtrahend)
976
 
 
977
 
        if isinstance(subtrahend, Time):
978
 
            return self.asDatetime() - subtrahend.asDatetime()
979
 
 
980
 
        return NotImplemented