2
# -*- coding: utf-8 -*-
5
# Copyright (c) 2009, 2010, Henry Precheur <henry@precheur.org>
7
# Permission to use, copy, modify, and/or distribute this software for any
8
# purpose with or without fee is hereby granted, provided that the above
9
# copyright notice and this permission notice appear in all copies.
11
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
12
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
13
# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
14
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
15
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
16
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17
# PERFORMANCE OF THIS SOFTWARE.
19
'''Formats dates according to the :RFC:`3339`.
21
Report bugs & problems on BitBucket_
23
.. _BitBucket: https://bitbucket.org/henry/clan.cx/issues
26
__author__ = 'Henry Precheur <henry@precheur.org>'
29
__all__ = ('rfc3339', )
35
def _timezone(utc_offset):
37
Return a string representing the timezone offset.
48
# Python's division uses floor(), not round() like in other languages:
49
# -1 / 2 == -1 and not -1 / 2 == 0
50
# That's why we use abs(utc_offset).
51
hours = abs(utc_offset) // 3600
52
minutes = abs(utc_offset) % 3600 // 60
53
sign = (utc_offset < 0 and '-') or '+'
54
return '%c%02d:%02d' % (sign, hours, minutes)
56
def _timedelta_to_seconds(timedelta):
58
>>> _timedelta_to_seconds(datetime.timedelta(hours=3))
60
>>> _timedelta_to_seconds(datetime.timedelta(hours=3, minutes=15))
63
return (timedelta.days * 86400 + timedelta.seconds +
64
timedelta.microseconds // 1000)
66
def _utc_offset(date, use_system_timezone):
68
Return the UTC offset of `date`. If `date` does not have any `tzinfo`, use
69
the timezone informations stored locally on the system.
71
>>> if time.localtime().tm_isdst:
72
... system_timezone = -time.altzone
74
... system_timezone = -time.timezone
75
>>> _utc_offset(datetime.datetime.now(), True) == system_timezone
77
>>> _utc_offset(datetime.datetime.now(), False)
80
if isinstance(date, datetime.datetime) and date.tzinfo is not None:
81
return _timedelta_to_seconds(date.dst() or date.utcoffset())
82
elif use_system_timezone:
84
# We use 1972 because 1970 doesn't have a leap day (feb 29)
85
t = time.mktime(date.replace(year=1972).timetuple())
87
t = time.mktime(date.timetuple())
88
if time.localtime(t).tm_isdst: # pragma: no cover
95
def _string(d, timezone):
96
return ('%04d-%02d-%02dT%02d:%02d:%02d%s' %
97
(d.year, d.month, d.day, d.hour, d.minute, d.second, timezone))
99
def rfc3339(date, utc=False, use_system_timezone=True):
101
Return a string formatted according to the :RFC:`3339`. If called with
102
`utc=True`, it normalizes `date` to the UTC date. If `date` does not have
103
any timezone information, uses the local timezone::
105
>>> d = datetime.datetime(2008, 4, 2, 20)
106
>>> rfc3339(d, utc=True, use_system_timezone=False)
107
'2008-04-02T20:00:00Z'
108
>>> rfc3339(d) # doctest: +ELLIPSIS
109
'2008-04-02T20:00:00...'
111
If called with `user_system_timezone=False` don't use the local timezone if
112
`date` does not have timezone informations and consider the offset to UTC
115
>>> rfc3339(d, use_system_timezone=False)
116
'2008-04-02T20:00:00+00:00'
118
`date` must be a `datetime.datetime`, `datetime.date` or a timestamp as
119
returned by `time.time()`::
121
>>> rfc3339(0, utc=True, use_system_timezone=False)
122
'1970-01-01T00:00:00Z'
123
>>> rfc3339(datetime.date(2008, 9, 6), utc=True,
124
... use_system_timezone=False)
125
'2008-09-06T00:00:00Z'
126
>>> rfc3339(datetime.date(2008, 9, 6),
127
... use_system_timezone=False)
128
'2008-09-06T00:00:00+00:00'
129
>>> rfc3339('foo bar')
130
Traceback (most recent call last):
132
TypeError: Expected timestamp or date object. Got <type 'str'>.
134
For dates before January 1st 1970, the timezones will be the ones used in
135
1970. It might not be accurate, but on most sytem there is no timezone
136
information before 1970.
138
# Try to convert timestamp to datetime
140
if use_system_timezone:
141
date = datetime.datetime.fromtimestamp(date)
143
date = datetime.datetime.utcfromtimestamp(date)
147
if not isinstance(date, datetime.date):
148
raise TypeError('Expected timestamp or date object. Got %r.' %
151
if not isinstance(date, datetime.datetime):
152
date = datetime.datetime(*date.timetuple()[:3])
153
utc_offset = _utc_offset(date, use_system_timezone)
155
return _string(date + datetime.timedelta(seconds=utc_offset), 'Z')
157
return _string(date, _timezone(utc_offset))
160
class LocalTimeTestCase(unittest.TestCase):
162
Test the use of the timezone saved locally. Since it is hard to test using
167
local_utcoffset = _utc_offset(datetime.datetime.now(), True)
168
self.local_utcoffset = datetime.timedelta(seconds=local_utcoffset)
169
self.local_timezone = _timezone(local_utcoffset)
171
def test_datetime(self):
172
d = datetime.datetime.now()
173
self.assertEqual(rfc3339(d),
174
d.strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone)
176
def test_datetime_timezone(self):
178
class FixedNoDst(datetime.tzinfo):
179
'A timezone info with fixed offset, not DST'
181
def utcoffset(self, dt):
182
return datetime.timedelta(hours=2, minutes=30)
187
fixed_no_dst = FixedNoDst()
189
class Fixed(FixedNoDst):
190
'A timezone info with DST'
193
return datetime.timedelta(hours=3, minutes=15)
197
d = datetime.datetime.now().replace(tzinfo=fixed_no_dst)
198
timezone = _timezone(_timedelta_to_seconds(fixed_no_dst.\
200
self.assertEqual(rfc3339(d),
201
d.strftime('%Y-%m-%dT%H:%M:%S') + timezone)
203
d = datetime.datetime.now().replace(tzinfo=fixed)
204
timezone = _timezone(_timedelta_to_seconds(fixed.dst(None)))
205
self.assertEqual(rfc3339(d),
206
d.strftime('%Y-%m-%dT%H:%M:%S') + timezone)
208
def test_datetime_utc(self):
209
d = datetime.datetime.now()
210
d_utc = d + self.local_utcoffset
211
self.assertEqual(rfc3339(d, utc=True),
212
d_utc.strftime('%Y-%m-%dT%H:%M:%SZ'))
215
d = datetime.date.today()
216
self.assertEqual(rfc3339(d),
217
d.strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone)
219
def test_date_utc(self):
220
d = datetime.date.today()
221
# Convert `date` to `datetime`, since `date` ignores seconds and hours
223
# >>> datetime.date(2008, 9, 7) + datetime.timedelta(hours=23)
224
# datetime.date(2008, 9, 7)
225
d_utc = datetime.datetime(*d.timetuple()[:3]) + self.local_utcoffset
226
self.assertEqual(rfc3339(d, utc=True),
227
d_utc.strftime('%Y-%m-%dT%H:%M:%SZ'))
229
def test_timestamp(self):
231
self.assertEqual(rfc3339(d),
232
datetime.datetime.fromtimestamp(d).\
233
strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone)
235
def test_timestamp_utc(self):
237
d_utc = datetime.datetime.utcfromtimestamp(d) + self.local_utcoffset
238
self.assertEqual(rfc3339(d),
239
(d_utc.strftime('%Y-%m-%dT%H:%M:%S') +
240
self.local_timezone))
242
def test_before_1970(self):
243
d = datetime.date(1885, 01, 04)
244
self.failUnless(rfc3339(d).startswith('1885-01-04T00:00:00'))
245
self.assertEqual(rfc3339(d, utc=True, use_system_timezone=False),
246
'1885-01-04T00:00:00Z')
249
d = datetime.date(1920, 02, 29)
250
x = rfc3339(d, utc=False, use_system_timezone=True)
251
self.failUnless(x.startswith('1920-02-29T00:00:00'))
253
# If these tests start failing it probably means there was a policy change
254
# for the Pacific time zone.
255
# See http://en.wikipedia.org/wiki/Pacific_Time_Zone.
256
if 'PST' in time.tzname:
257
def testPDTChange(self):
258
'''Test Daylight saving change'''
259
# PDT switch happens at 2AM on March 14, 2010
262
self.assertEqual(rfc3339(datetime.datetime(2010, 3, 14, 1, 59)),
263
'2010-03-14T01:59:00-08:00')
265
self.assertEqual(rfc3339(datetime.datetime(2010, 3, 14, 3, 0)),
266
'2010-03-14T03:00:00-07:00')
268
def testPSTChange(self):
269
'''Test Standard time change'''
270
# PST switch happens at 2AM on November 6, 2010
273
self.assertEqual(rfc3339(datetime.datetime(2010, 11, 7, 0, 59)),
274
'2010-11-07T00:59:00-07:00')
277
# There's no way to have 1:00AM PST without a proper tzinfo
278
self.assertEqual(rfc3339(datetime.datetime(2010, 11, 7, 1, 0)),
279
'2010-11-07T01:00:00-07:00')
282
if __name__ == '__main__': # pragma: no cover