~divmod-dev/divmod.org/dangling-1091

« back to all changes in this revision

Viewing changes to Axiom/axiom/timex.py

  • Committer: glyph
  • Date: 2005-07-28 22:09:16 UTC
  • Revision ID: svn-v4:866e43f7-fbfc-0310-8f2a-ec88d1da2979:trunk:2
move this repository to a more official-looking URL

Show diffs side-by-side

added added

removed removed

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