3
Extended date/time formatting and miscellaneous functionality.
5
See the class 'Time' for details.
12
from email.Utils import mktime_tz, parsedate_tz
14
class FixedOffset(datetime.tzinfo):
15
_zeroOffset = datetime.timedelta()
17
def __init__(self, hours, minutes):
18
self.offset = datetime.timedelta(minutes = hours * 60 + minutes)
20
def utcoffset(self, dt):
24
return Time._timedeltaToHrMin(self.offset)
27
return self._zeroOffset
30
return '<%s.%s object at 0x%x offset %r>' % (self.__module__, type(self).__name__, id(self), self.offset)
35
"""An object representing a well defined instant in time.
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.
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.
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
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.
72
For containment tests with the 'in' operator, the period addressed by the
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.
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.
83
For formats with both a constructor and a formatter, d == fromFu(d.asFu())
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
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.
96
rfc2822Weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
97
rfc2822Months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
99
resolution = datetime.timedelta.resolution
102
# Methods to create new instances
106
"""Return a new Time instance representing the time now.
108
See also the fromFu methods to create new instances from other types of
111
self._time = datetime.datetime.utcnow()
113
def _timedeltaToHrMin(offset):
114
"""Return a (sign, hour, minute) triple coresponding to the offset described by timedelta.
116
sign is "+" or "-". In the case of 0 offset, sign is "+".
118
minutes = round((offset.days * 3600000000 * 24 + offset.seconds * 1000000 + offset.microseconds) / 60000000.0)
124
return (sign, minutes // 60, minutes % 60)
126
_timedeltaToHrMin = staticmethod(_timedeltaToHrMin)
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:
135
self = klass.fromDatetime(dtnow + datetime.timedelta(days=daysInFuture))
136
assert self.asDatetime().weekday() == weekday
137
self.resolution = datetime.timedelta(days=1)
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)
149
assert when == 'today'
150
self = klass.fromDatetime(dtnow)
151
self.resolution = datetime.timedelta(days=1)
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()
160
if not 1 <= hour <= 12:
161
raise ValueError, 'hour %i is not in 1..12' % (hour,)
162
if hour == 12 and ampm == 'am':
166
if not 0 <= hour <= 23:
167
raise ValueError, 'hour %i is not in 0..23' % (hour,)
169
dtnow = now.asDatetime(tzinfo).replace(second=0, microsecond=0)
170
dtthen = dtnow.replace(hour=hour, minute=minute)
172
dtthen += datetime.timedelta(days=1)
174
self = klass.fromDatetime(dtthen)
175
self.resolution = datetime.timedelta(minutes=1)
179
def _fromNoonOrMidnight(klass, match, tzinfo, now):
180
when = match.group(0).lower()
184
assert when == 'midnight'
186
dtnow = now.asDatetime(tzinfo).replace(minute=0, second=0, microsecond=0)
187
dtthen = dtnow.replace(hour=hour)
189
dtthen += datetime.timedelta(days=1)
191
self = klass.fromDatetime(dtthen)
192
self.resolution = datetime.timedelta(minutes=1)
196
weekdays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
212
""", re.IGNORECASE | re.VERBOSE),
214
(re.compile(r"\b(today|tomorrow|yesterday)\b", re.IGNORECASE),
215
_fromTodayOrTomorrow),
218
(?P<hour>\d{1,2}):(?P<minute>\d{2})
219
(\s*(?P<ampm>am|pm))?
221
""", re.IGNORECASE | re.VERBOSE),
223
(re.compile(r"\b(noon|midnight)\b", re.IGNORECASE),
224
_fromNoonOrMidnight),
227
_fromWeekday = classmethod(_fromWeekday)
228
_fromTodayOrTomorrow = classmethod(_fromTodayOrTomorrow)
229
_fromTime = classmethod(_fromTime)
230
_fromNoonOrMidnight = classmethod(_fromNoonOrMidnight)
233
def fromHumanly(klass, humanStr, tzinfo=None, now=None):
234
"""Return a new Time instance from a string a human might type.
236
@param humanStr: the string to be parsed.
238
@param tzinfo: A tzinfo instance indicating the timezone to assume if
239
none is specified in humanStr. If None, assume UTC.
241
@param now: A Time instance to be considered "now" for when
242
interpreting relative dates like "tomorrow". If None, use the real now.
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.
248
humanStr = humanStr.strip()
252
tzinfo = FixedOffset(0, 0)
254
for pattern, creator in klass.humanlyPatterns:
255
match = pattern.match(humanStr)
257
or match.span()[1] != len(humanStr):
260
return creator(klass, match, tzinfo, now)
263
raise ValueError, 'could not parse date: %r' % (humanStr,)
265
fromHumanly = classmethod(fromHumanly)
268
iso8601pattern = re.compile(r"""
271
# a year may optionally be followed by one of:
274
# - a specific day, and an optional time
275
# a specific day is one of:
278
# - a day of the year
280
-? (?P<month1> \d{2})
282
-? W (?P<week1> \d{2})
285
-? (?P<month2> \d{2})
288
-? W (?P<week2> \d{2})
291
-? (?P<dayofyear> \d{3})
296
:? (?P<minute> \d{2})
298
:? (?P<second> \d{2})
300
[\.,] (?P<fractionalsec> \d+)
307
(?P<tzhour> [+\-]\d{2})
317
def fromISO8601TimeAndDate(klass, iso8601string, tzinfo=None):
318
"""Return a new Time instance from a string formated as in ISO 8601.
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.
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`.
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.
333
def calculateTimezone():
334
if groups['zulu'] == 'Z':
335
return FixedOffset(0, 0)
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)
344
groups['month'] = groups['month1'] or groups['month2']
345
groups['week'] = groups['week1'] or groups['week2']
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:
354
for key in defaultTo1:
355
if groups[key] is None:
357
groups['fractionalsec'] = float('.'+groups['fractionalsec'])
358
for key in defaultTo0 + defaultTo1 + ['year']:
359
groups[key] = int(groups[key])
361
for group, min, max in [
362
('week', 1, 53), # some years have only 52
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)
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 ) )
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:
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
397
nextYear = self._time.replace(year=self._time.year+1)
398
return nextYear - self._time
400
def calculateDtime(tzinfo):
401
"""Calculate a datetime for the start of the addressed period."""
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(
414
int(round(groups['fractionalsec'] * 1000000)),
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'])
426
if match.group('dayofyear') is not None:
427
dtime = datetime.datetime(
434
int(round(groups['fractionalsec'] * 1000000)),
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'])
443
return datetime.datetime(
450
int(round(groups['fractionalsec'] * 1000000)),
455
match = klass.iso8601pattern.match(iso8601string)
457
raise ValueError, '%r could not be parsed as an ISO 8601 date and time' % (iso8601string,)
459
groups = match.groupdict()
461
if match.group('hour') is not None:
462
timezone = calculateTimezone()
465
self = klass.fromDatetime(calculateDtime(timezone))
466
self.resolution = determineResolution()
469
fromISO8601TimeAndDate = classmethod(fromISO8601TimeAndDate)
471
def fromStructTime(klass, structTime, tzinfo=None):
472
"""Return a new Time instance from a time.struct_time.
474
If tzinfo is None, structTime is in UTC. Otherwise, tzinfo is a
475
datetime.tzinfo instance coresponding to the timezone in which
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
483
dtime = datetime.datetime(tzinfo=tzinfo, *structTime[:6])
484
self = klass.fromDatetime(dtime)
485
self.resolution = datetime.timedelta(seconds=1)
488
fromStructTime = classmethod(fromStructTime)
490
def fromDatetime(klass, dtime):
491
"""Return a new Time instance from a datetime.datetime instance.
493
If the datetime instance does not have an associated timezone, it is
496
self = klass.__new__(klass)
497
if dtime.tzinfo is not None:
498
self._time = dtime.astimezone(FixedOffset(0, 0)).replace(tzinfo=None)
501
self.resolution = datetime.timedelta.resolution
504
fromDatetime = classmethod(fromDatetime)
506
def fromPOSIXTimestamp(klass, secs):
507
"""Return a new Time instance from seconds since the POSIX epoch.
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
513
`secs` can be an integer or a float.
515
self = klass.fromDatetime(datetime.datetime.utcfromtimestamp(secs))
516
self.resolution = datetime.timedelta()
519
fromPOSIXTimestamp = classmethod(fromPOSIXTimestamp)
521
def fromRFC2822(klass, rfc822string):
522
"""Return a new Time instance from a string formated as described in RFC 2822.
524
RFC 2822 specifies the format of email messages.
526
Some of the obsoleted elements of the specification are not parsed
527
correctly, and will raise ValueError.
529
date = parsedate_tz(rfc822string)
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)
536
fromRFC2822 = classmethod(fromRFC2822)
539
# Methods to produce various formats
542
def asPOSIXTimestamp(self):
543
"""Return this time as a timestamp as specified by POSIX.
545
This timestamp is the count of the number of seconds since Midnight,
546
Jan 1 1970 UTC, ignoring leap seconds.
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
554
def asDatetime(self, tzinfo=None):
555
"""Return this time as an aware datetime.datetime instance.
557
The returned datetime object has the specified tzinfo, or a tzinfo
558
describing UTC if the tzinfo parameter is None.
561
tzinfo = FixedOffset(0, 0)
563
if not self.isTimezoneDependent():
564
return self._time.replace(tzinfo=tzinfo)
566
return self._time.replace(tzinfo=FixedOffset(0, 0)).astimezone(tzinfo)
568
def asNaiveDatetime(self, tzinfo=None):
569
"""Return this time as a naive datetime.datetime instance.
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
575
return self.asDatetime(tzinfo).replace(tzinfo=None)
577
def asRFC2822(self, tzinfo=None, includeDayOfWeek=True):
578
"""Return this Time formatted as specified in RFC 2822.
580
RFC 2822 specifies the format of email messages.
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
588
RFC 2822 states that the weekday is optional. The parameter
589
includeDayOfWeek indicates whether or not to include it.
591
dtime = self.asDatetime(tzinfo)
596
rfcoffset = '%s%02i%02i' % self._timedeltaToHrMin(dtime.utcoffset())
600
rfcstring += self.rfc2822Weekdays[dtime.weekday()] + ', '
602
rfcstring += '%i %s %4i %02i:%02i:%02i %s' % (
604
self.rfc2822Months[dtime.month - 1],
613
def asISO8601TimeAndDate(self, includeDelimiters=True, tzinfo=None, includeTimezone=True):
614
"""Return this time formatted as specified by ISO 8861.
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.
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.
624
The includeTimezone parameter coresponds to the inclusion of an
625
explicit timezone. The default is True.
627
if not self.isTimezoneDependent():
629
dtime = self.asDatetime(tzinfo)
631
if includeDelimiters:
635
dateSep = timeSep = ''
639
timezone = '+00%s00' % (timeSep,)
641
sign, hour, min = self._timedeltaToHrMin(dtime.utcoffset())
642
timezone = '%s%02i%s%02i' % (sign, hour, timeSep, min)
646
microsecond = ('%06i' % (dtime.microsecond,)).rstrip('0')
648
microsecond = '.' + microsecond
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))
663
for part, minResolution in parts:
664
if self.resolution <= minResolution:
669
def asStructTime(self, tzinfo=None):
670
"""Return this time represented as a time.struct_time.
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.
675
dtime = self.asDatetime(tzinfo)
677
return dtime.utctimetuple()
679
return dtime.timetuple()
681
def asHumanly(self, tzinfo=None, now=None):
682
"""Return this time as a short string, tailored to the current time.
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.
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'.
693
now = Time().asDatetime(tzinfo)
695
now = now.asDatetime(tzinfo)
696
dtime = self.asDatetime(tzinfo)
698
if dtime.date() == now.date():
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
707
res = dtime.strftime('%b %y, %I:%M %p').lower().capitalize()
708
res = str(dtime.date().day) + ' ' + res
712
# methods to return related times
715
def getBounds(self, tzinfo=None):
716
"""Return a pair describing the bounds of self.
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
723
To illustrate the problem, consider a Time instance:
725
T = Time.fromHumanly('today', tzinfo=anything)
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
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.
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.
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'.
746
if self.resolution >= datetime.timedelta(days=1) \
747
and tzinfo is not None:
748
time = self._time.replace(tzinfo=tzinfo)
753
min(self.fromDatetime(time), self.fromDatetime(self._time)),
754
max(self.fromDatetime(time + self.resolution), self.fromDatetime(self._time + self.resolution))
758
"""Return a Time instance representing the day of the start of self.
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).
764
day = self.__class__.fromDatetime(self.asDatetime().replace(hour=0, minute=0, second=0, microsecond=0))
765
day.resolution = datetime.timedelta(days=1)
773
"""Return True iff this instance represents exactly all day."""
774
return self.resolution == datetime.timedelta(days=1)
776
def isTimezoneDependent(self):
777
"""Return True iff timezone is relevant for this instance.
779
Timezone is only relevent for instances with a resolution better than
782
return self.resolution < datetime.timedelta(days=1)
785
# other magic methods
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)
793
def __eq__(self, other):
794
if isinstance(other, Time):
795
return cmp(self._time, other._time) == 0
798
def __ne__(self, other):
799
return not (self == other)
802
return 'tpython.Time.fromDatetime(%r)' % (self._time,)
804
__str__ = asISO8601TimeAndDate
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:
812
if self._time + self.resolution < other._time + other.resolution:
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)
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)