~matias-wilkman/calendar-indicator/fix-typo-and-tautology

« back to all changes in this revision

Viewing changes to src/rfc3339.py

  • Committer: Lorenzo Carbonell
  • Date: 2012-11-24 08:51:01 UTC
  • Revision ID: lorenzo.carbonell.cerezo@gmail.com-20121124085101-2kfixu1jmas4mknw
request + Google Calendar API 3.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
# -*- coding: utf-8 -*-
3
 
#
4
 
#
5
 
# Copyright (c) 2009, 2010, Henry Precheur <henry@precheur.org>
6
 
#
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.
10
 
#
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.
18
 
#
19
 
'''Formats dates according to the :RFC:`3339`.
20
 
 
21
 
Report bugs & problems on BitBucket_
22
 
 
23
 
.. _BitBucket: https://bitbucket.org/henry/clan.cx/issues
24
 
'''
25
 
 
26
 
__author__ = 'Henry Precheur <henry@precheur.org>'
27
 
__license__ = 'ISCL'
28
 
__version__ = '5.1'
29
 
__all__ = ('rfc3339', )
30
 
 
31
 
import datetime
32
 
import time
33
 
import unittest
34
 
 
35
 
def _timezone(utc_offset):
36
 
    '''
37
 
    Return a string representing the timezone offset.
38
 
 
39
 
    >>> _timezone(0)
40
 
    '+00:00'
41
 
    >>> _timezone(3600)
42
 
    '+01:00'
43
 
    >>> _timezone(-28800)
44
 
    '-08:00'
45
 
    >>> _timezone(-1800)
46
 
    '-00:30'
47
 
    '''
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)
55
 
 
56
 
def _timedelta_to_seconds(timedelta):
57
 
    '''
58
 
    >>> _timedelta_to_seconds(datetime.timedelta(hours=3))
59
 
    10800
60
 
    >>> _timedelta_to_seconds(datetime.timedelta(hours=3, minutes=15))
61
 
    11700
62
 
    '''
63
 
    return (timedelta.days * 86400 + timedelta.seconds +
64
 
            timedelta.microseconds // 1000)
65
 
 
66
 
def _utc_offset(date, use_system_timezone):
67
 
    '''
68
 
    Return the UTC offset of `date`. If `date` does not have any `tzinfo`, use
69
 
    the timezone informations stored locally on the system.
70
 
 
71
 
    >>> if time.localtime().tm_isdst:
72
 
    ...     system_timezone = -time.altzone
73
 
    ... else:
74
 
    ...     system_timezone = -time.timezone
75
 
    >>> _utc_offset(datetime.datetime.now(), True) == system_timezone
76
 
    True
77
 
    >>> _utc_offset(datetime.datetime.now(), False)
78
 
    0
79
 
    '''
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:
83
 
        if date.year < 1970:
84
 
            # We use 1972 because 1970 doesn't have a leap day (feb 29)
85
 
            t = time.mktime(date.replace(year=1972).timetuple())
86
 
        else:
87
 
            t = time.mktime(date.timetuple())
88
 
        if time.localtime(t).tm_isdst: # pragma: no cover
89
 
            return -time.altzone
90
 
        else:
91
 
            return -time.timezone
92
 
    else:
93
 
        return 0
94
 
 
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))
98
 
 
99
 
def rfc3339(date, utc=False, use_system_timezone=True):
100
 
    '''
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::
104
 
 
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...'
110
 
 
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
113
 
    to be zero::
114
 
 
115
 
        >>> rfc3339(d, use_system_timezone=False)
116
 
        '2008-04-02T20:00:00+00:00'
117
 
 
118
 
    `date` must be a `datetime.datetime`, `datetime.date` or a timestamp as
119
 
    returned by `time.time()`::
120
 
 
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):
131
 
        ...
132
 
        TypeError: Expected timestamp or date object. Got <type 'str'>.
133
 
 
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.
137
 
    '''
138
 
    # Try to convert timestamp to datetime
139
 
    try:
140
 
        if use_system_timezone:
141
 
            date = datetime.datetime.fromtimestamp(date)
142
 
        else:
143
 
            date = datetime.datetime.utcfromtimestamp(date)
144
 
    except TypeError:
145
 
        pass
146
 
 
147
 
    if not isinstance(date, datetime.date):
148
 
        raise TypeError('Expected timestamp or date object. Got %r.' %
149
 
                        type(date))
150
 
 
151
 
    if not isinstance(date, datetime.datetime):
152
 
        date = datetime.datetime(*date.timetuple()[:3])
153
 
    utc_offset = _utc_offset(date, use_system_timezone)
154
 
    if utc:
155
 
        return _string(date + datetime.timedelta(seconds=utc_offset), 'Z')
156
 
    else:
157
 
        return _string(date, _timezone(utc_offset))
158
 
 
159
 
 
160
 
class LocalTimeTestCase(unittest.TestCase):
161
 
    '''
162
 
    Test the use of the timezone saved locally. Since it is hard to test using
163
 
    doctest.
164
 
    '''
165
 
 
166
 
    def setUp(self):
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)
170
 
 
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)
175
 
 
176
 
    def test_datetime_timezone(self):
177
 
 
178
 
        class FixedNoDst(datetime.tzinfo):
179
 
            'A timezone info with fixed offset, not DST'
180
 
 
181
 
            def utcoffset(self, dt):
182
 
                return datetime.timedelta(hours=2, minutes=30)
183
 
 
184
 
            def dst(self, dt):
185
 
                return None
186
 
 
187
 
        fixed_no_dst = FixedNoDst()
188
 
 
189
 
        class Fixed(FixedNoDst):
190
 
            'A timezone info with DST'
191
 
 
192
 
            def dst(self, dt):
193
 
                return datetime.timedelta(hours=3, minutes=15)
194
 
 
195
 
        fixed = Fixed()
196
 
 
197
 
        d = datetime.datetime.now().replace(tzinfo=fixed_no_dst)
198
 
        timezone = _timezone(_timedelta_to_seconds(fixed_no_dst.\
199
 
                                                   utcoffset(None)))
200
 
        self.assertEqual(rfc3339(d),
201
 
                         d.strftime('%Y-%m-%dT%H:%M:%S') + timezone)
202
 
 
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)
207
 
 
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'))
213
 
 
214
 
    def test_date(self):
215
 
        d = datetime.date.today()
216
 
        self.assertEqual(rfc3339(d),
217
 
                         d.strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone)
218
 
 
219
 
    def test_date_utc(self):
220
 
        d = datetime.date.today()
221
 
        # Convert `date` to `datetime`, since `date` ignores seconds and hours
222
 
        # in timedeltas:
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'))
228
 
 
229
 
    def test_timestamp(self):
230
 
        d = time.time()
231
 
        self.assertEqual(rfc3339(d),
232
 
                         datetime.datetime.fromtimestamp(d).\
233
 
                         strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone)
234
 
 
235
 
    def test_timestamp_utc(self):
236
 
        d = time.time()
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))
241
 
 
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')
247
 
 
248
 
    def test_1920(self):
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'))
252
 
 
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
260
 
 
261
 
            # 1:59AM PST
262
 
            self.assertEqual(rfc3339(datetime.datetime(2010, 3, 14, 1, 59)),
263
 
                             '2010-03-14T01:59:00-08:00')
264
 
            # 3AM PDT
265
 
            self.assertEqual(rfc3339(datetime.datetime(2010, 3, 14, 3, 0)),
266
 
                             '2010-03-14T03:00:00-07:00')
267
 
 
268
 
        def testPSTChange(self):
269
 
            '''Test Standard time change'''
270
 
            # PST switch happens at 2AM on November 6, 2010
271
 
 
272
 
            # 0:59AM PDT
273
 
            self.assertEqual(rfc3339(datetime.datetime(2010, 11, 7, 0, 59)),
274
 
                             '2010-11-07T00:59:00-07:00')
275
 
 
276
 
            # 1:00AM PST
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')
280
 
 
281
 
 
282
 
if __name__ == '__main__': # pragma: no cover
283
 
    import doctest
284
 
    doctest.testmod()
285
 
    unittest.main()