• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / zope.datetime / 4708140782

pending completion
4708140782

push

github

GitHub
Merge pull request #19 from zopefoundation/config-with-pure-python-template-2288b028

251 of 260 branches covered (96.54%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 3 files covered. (100.0%)

828 of 829 relevant lines covered (99.88%)

1.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.48
/src/zope/datetime/__init__.py
1
##############################################################################
2
#
3
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14
"""Commonly used utility functions.
1✔
15

16
Encapsulation of date/time values
17
"""
18

19
import math
1✔
20
import re
1✔
21
# there is a method definition that makes just "time"
22
# problematic while executing a class definition
23
import time as _time
1✔
24
from datetime import datetime as _datetime
1✔
25
from datetime import timedelta as _timedelta
1✔
26
from datetime import tzinfo as _std_tzinfo
1✔
27
from time import tzname
1✔
28

29
from zope.datetime.timezones import historical_zone_info as _data
1✔
30

31

32
if str is bytes:  # PY2
1!
33
    StringTypes = (basestring,)  # noqa: F821 undefined name pragma: PY2
×
34
else:
35
    StringTypes = (str,)  # pragma: PY3
1✔
36

37

38
# These are needed because the various date formats below must
39
# be in english per the RFCs. That means we can't use strftime,
40
# which is affected by different locale settings.
41
weekday_abbr = [
1✔
42
    'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun',
43
]
44
weekday_full = [
1✔
45
    'Monday', 'Tuesday', 'Wednesday', 'Thursday',
46
    'Friday', 'Saturday', 'Sunday',
47
]
48
monthname = [
1✔
49
    None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
50
    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
51
]
52

53

54
def _get_gmtime_compatible_timestamp(ts):
1✔
55
    """Try to convert any argument to a timestamp ``time.gmtime`` can use."""
56
    if ts is None:
1✔
57
        # ``time.gmtime`` uses the current time but only if not given any
58
        # parameter. It is easier to compute current time here:
59
        ts = _time.time()
1✔
60
    else:
61
        # ``time.gmtime`` ignores fractions of seconds. Before Python 3.10 it
62
        # used to convert its argument to an ``int`` if it was not a number.
63
        ts = int(ts)
1✔
64
    return ts
1✔
65

66

67
def iso8601_date(ts=None):
1✔
68
    """
69
    Return an ISO 8601 formatted date string, required
70
    for certain DAV properties.
71

72
    For example: '2000-11-10T16:21:09-08:00'
73

74
    :param any ts: A timestamp as returned by :func:`time.time` (``float``),
75
        seconds since the epoch (``int``) or any object which can be converted
76
        to ``int`` via a ``__int__`` method returning number of seconds since
77
        the epoch.
78
        If not given, the current time will be used.
79
    """
80
    ts = _get_gmtime_compatible_timestamp(ts)
1✔
81
    return _time.strftime('%Y-%m-%dT%H:%M:%SZ', _time.gmtime(ts))
1✔
82

83

84
def rfc850_date(ts=None):
1✔
85
    """
86
    Return an RFC 850 formatted date string.
87

88
    For example, 'Friday, 10-Nov-00 16:21:09 GMT'.
89

90
    :param any ts: A timestamp as returned by :func:`time.time` (``float``),
91
        seconds since the epoch (``int``) or any object which can be converted
92
        to ``int`` via a ``__int__`` method returning number of seconds since
93
        the epoch.
94
    """
95
    ts = _get_gmtime_compatible_timestamp(ts)
1✔
96
    year, month, day, hh, mm, ss, wd, _y, _z = _time.gmtime(ts)
1✔
97
    return "%s, %02d-%3s-%2s %02d:%02d:%02d GMT" % (
1✔
98
        weekday_full[wd],
99
        day, monthname[month],
100
        str(year)[2:],
101
        hh, mm, ss
102
    )
103

104

105
def rfc1123_date(ts=None):
1✔
106
    """
107
    Return an RFC 1123 format date string, required for
108
    use in HTTP Date headers per the HTTP 1.1 spec.
109

110
    For example, 'Fri, 10 Nov 2000 16:21:09 GMT'.
111

112
    :param any ts: A timestamp as returned by :func:`time.time` (``float``),
113
        seconds since the epoch (``int``) or any object which can be converted
114
        to ``int`` via a ``__int__`` method returning number of seconds since
115
        the epoch.
116
    """
117
    ts = _get_gmtime_compatible_timestamp(ts)
1✔
118
    year, month, day, hh, mm, ss, wd, _y, _z = _time.gmtime(ts)
1✔
119
    return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
1✔
120
        weekday_abbr[wd],
121
        day, monthname[month],
122
        year,
123
        hh, mm, ss)
124

125

126
class DateTimeError(Exception):
1✔
127
    """
128
    The root exception for errors raised by this module.
129
    """
130

131

132
class DateError(DateTimeError):
1✔
133
    """
134
    Invalid Date Components
135
    """
136

137

138
class TimeError(DateTimeError):
1✔
139
    """
140
    Invalid Time Components
141
    """
142

143

144
class SyntaxError(DateTimeError):
1✔
145
    """
146
    Invalid Date-Time String
147
    """
148

149

150
# Determine machine epoch
151
def _calc_epoch():
1✔
152
    tm = ((0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334),
1✔
153
          (0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335))
154
    yr, mo, dy, hr, mn, sc = _time.gmtime(0)[:6]
1✔
155
    i = int(yr - 1)
1✔
156
    to_year = int(i * 365 + i / 4 - i / 100 + i / 400 - 693960.0)
1✔
157
    to_month = tm[yr % 4 == 0 and (yr % 100 != 0 or yr % 400 == 0)][mo]
1✔
158
    epoch = (to_year + to_month + dy +
1✔
159
             (hr / 24.0 + mn / 1440.0 + sc / 86400.0)) * 86400
160
    return epoch
1✔
161

162

163
EPOCH = _calc_epoch()
1✔
164
jd1901 = 2415385
1✔
165

166
numericTimeZoneMatch = re.compile(r'[+-][0-9][0-9][0-9][0-9]').match  # TS
1✔
167

168

169
class _timezone:
1✔
170

171
    def __init__(self, data):
1✔
172
        self.name, self.timect, self.typect, \
1✔
173
            ttrans, self.tindex, self.tinfo, self.az = data
174
        self.ttrans = [int(tt) for tt in ttrans]
1✔
175

176
    def default_index(self):
1✔
177
        if self.timect == 0:
1✔
178
            return 0
1✔
179
        for i in range(self.typect):
1✔
180
            if self.tinfo[i][1] == 0:
1✔
181
                return i
1✔
182
        return 0
1✔
183

184
    def index(self, t=None):
1✔
185
        t = t if t is not None else _time.time()
1✔
186
        if self.timect == 0:
1✔
187
            idx = (0, 0, 0)
1✔
188
        elif t < self.ttrans[0]:
1✔
189
            i = self.default_index()
1✔
190
            idx = (i, ord(self.tindex[0]), i)
1✔
191
        elif t >= self.ttrans[-1]:
1✔
192
            if self.timect > 1:
1✔
193
                idx = (ord(self.tindex[-1]), ord(self.tindex[-1]),
1✔
194
                       ord(self.tindex[-2]))
195
            else:
196
                idx = (ord(self.tindex[-1]), ord(self.tindex[-1]),
1✔
197
                       self.default_index())
198
        else:
199
            for i in range(self.timect - 1):
1!
200
                if t < self.ttrans[i + 1]:
1✔
201
                    idx = (ord(self.tindex[i]),
1✔
202
                           ord(self.tindex[i + 1]),
203
                           self.default_index() if i == 0
204
                           else ord(self.tindex[i - 1]))
205
                    break
1✔
206
        return idx
1✔
207

208
    def info(self, t=None):
1✔
209
        idx = self.index(t)[0]
1✔
210
        zs = self.az[self.tinfo[idx][2]:]
1✔
211
        return self.tinfo[idx][0], self.tinfo[idx][1], zs[: zs.find('\000')]
1✔
212

213

214
class _cache:
1✔
215

216
    _zlst = [
1✔
217
        'Brazil/Acre', 'Brazil/DeNoronha', 'Brazil/East',
218
        'Brazil/West', 'Canada/Atlantic', 'Canada/Central',
219
        'Canada/Eastern', 'Canada/East-Saskatchewan',
220
        'Canada/Mountain', 'Canada/Newfoundland',
221
        'Canada/Pacific', 'Canada/Yukon',
222
        'Chile/Continental', 'Chile/EasterIsland', 'CST', 'Cuba',
223
        'Egypt', 'EST', 'GB-Eire', 'Greenwich', 'Hongkong', 'Iceland',
224
        'Iran', 'Israel', 'Jamaica', 'Japan', 'Mexico/BajaNorte',
225
        'Mexico/BajaSur', 'Mexico/General', 'MST', 'Poland', 'PST',
226
        'Singapore', 'Turkey', 'Universal', 'US/Alaska', 'US/Aleutian',
227
        'US/Arizona', 'US/Central', 'US/Eastern', 'US/East-Indiana',
228
        'US/Hawaii', 'US/Indiana-Starke', 'US/Michigan',
229
        'US/Mountain', 'US/Pacific', 'US/Samoa', 'UTC', 'UCT', 'GMT',
230

231
        'GMT+0100', 'GMT+0200', 'GMT+0300', 'GMT+0400', 'GMT+0500',
232
        'GMT+0600', 'GMT+0700', 'GMT+0800', 'GMT+0900', 'GMT+1000',
233
        'GMT+1100', 'GMT+1200', 'GMT+1300', 'GMT-0100', 'GMT-0200',
234
        'GMT-0300', 'GMT-0400', 'GMT-0500', 'GMT-0600', 'GMT-0700',
235
        'GMT-0800', 'GMT-0900', 'GMT-1000', 'GMT-1100', 'GMT-1200',
236
        'GMT+1',
237

238
        'GMT+0130', 'GMT+0230', 'GMT+0330', 'GMT+0430', 'GMT+0530',
239
        'GMT+0630', 'GMT+0730', 'GMT+0830', 'GMT+0930', 'GMT+1030',
240
        'GMT+1130', 'GMT+1230',
241

242
        'GMT-0130', 'GMT-0230', 'GMT-0330', 'GMT-0430', 'GMT-0530',
243
        'GMT-0630', 'GMT-0730', 'GMT-0830', 'GMT-0930', 'GMT-1030',
244
        'GMT-1130', 'GMT-1230',
245

246
        'UT', 'BST', 'MEST', 'SST', 'FST', 'WADT', 'EADT', 'NZDT',
247
        'WET', 'WAT', 'AT', 'AST', 'NT', 'IDLW', 'CET', 'MET',
248
        'MEWT', 'SWT', 'FWT', 'EET', 'EEST', 'BT', 'ZP4', 'ZP5', 'ZP6',
249
        'WAST', 'CCT', 'JST', 'EAST', 'GST', 'NZT', 'NZST', 'IDLE'
250
    ]
251

252
    _zmap = {
1✔
253
        'aest': 'GMT+1000', 'aedt': 'GMT+1100',
254
        'aus eastern standard time': 'GMT+1000',
255
        'sydney standard time': 'GMT+1000',
256
        'tasmania standard time': 'GMT+1000',
257
        'e. australia standard time': 'GMT+1000',
258
        'aus central standard time': 'GMT+0930',
259
        'cen. australia standard time': 'GMT+0930',
260
        'w. australia standard time': 'GMT+0800',
261

262
        'brazil/acre': 'Brazil/Acre',
263
        'brazil/denoronha': 'Brazil/Denoronha',
264
        'brazil/east': 'Brazil/East', 'brazil/west': 'Brazil/West',
265
        'canada/atlantic': 'Canada/Atlantic',
266
        'canada/central': 'Canada/Central',
267
        'canada/eastern': 'Canada/Eastern',
268
        'canada/east-saskatchewan': 'Canada/East-Saskatchewan',
269
        'canada/mountain': 'Canada/Mountain',
270
        'canada/newfoundland': 'Canada/Newfoundland',
271
        'canada/pacific': 'Canada/Pacific', 'canada/yukon': 'Canada/Yukon',
272
        'central europe standard time': 'GMT+0100',
273
        'chile/continental': 'Chile/Continental',
274
        'chile/easterisland': 'Chile/EasterIsland',
275
        'cst': 'US/Central', 'cuba': 'Cuba', 'est': 'US/Eastern',
276
        'egypt': 'Egypt',
277
        'eastern standard time': 'US/Eastern',
278
        'us eastern standard time': 'US/Eastern',
279
        'central standard time': 'US/Central',
280
        'mountain standard time': 'US/Mountain',
281
        'pacific standard time': 'US/Pacific',
282
        'gb-eire': 'GB-Eire', 'gmt': 'GMT',
283

284
        'gmt+0000': 'GMT+0', 'gmt+0': 'GMT+0',
285

286

287
        'gmt+0100': 'GMT+1', 'gmt+0200': 'GMT+2', 'gmt+0300': 'GMT+3',
288
        'gmt+0400': 'GMT+4', 'gmt+0500': 'GMT+5', 'gmt+0600': 'GMT+6',
289
        'gmt+0700': 'GMT+7', 'gmt+0800': 'GMT+8', 'gmt+0900': 'GMT+9',
290
        'gmt+1000': 'GMT+10', 'gmt+1100': 'GMT+11', 'gmt+1200': 'GMT+12',
291
        'gmt+1300': 'GMT+13',
292
        'gmt-0100': 'GMT-1', 'gmt-0200': 'GMT-2', 'gmt-0300': 'GMT-3',
293
        'gmt-0400': 'GMT-4', 'gmt-0500': 'GMT-5', 'gmt-0600': 'GMT-6',
294
        'gmt-0700': 'GMT-7', 'gmt-0800': 'GMT-8', 'gmt-0900': 'GMT-9',
295
        'gmt-1000': 'GMT-10', 'gmt-1100': 'GMT-11', 'gmt-1200': 'GMT-12',
296

297
        'gmt+1': 'GMT+1', 'gmt+2': 'GMT+2', 'gmt+3': 'GMT+3',
298
        'gmt+4': 'GMT+4', 'gmt+5': 'GMT+5', 'gmt+6': 'GMT+6',
299
        'gmt+7': 'GMT+7', 'gmt+8': 'GMT+8', 'gmt+9': 'GMT+9',
300
        'gmt+10': 'GMT+10', 'gmt+11': 'GMT+11', 'gmt+12': 'GMT+12',
301
        'gmt+13': 'GMT+13',
302
        'gmt-1': 'GMT-1', 'gmt-2': 'GMT-2', 'gmt-3': 'GMT-3',
303
        'gmt-4': 'GMT-4', 'gmt-5': 'GMT-5', 'gmt-6': 'GMT-6',
304
        'gmt-7': 'GMT-7', 'gmt-8': 'GMT-8', 'gmt-9': 'GMT-9',
305
        'gmt-10': 'GMT-10', 'gmt-11': 'GMT-11', 'gmt-12': 'GMT-12',
306

307
        'gmt+130': 'GMT+0130', 'gmt+0130': 'GMT+0130',
308
        'gmt+230': 'GMT+0230', 'gmt+0230': 'GMT+0230',
309
        'gmt+330': 'GMT+0330', 'gmt+0330': 'GMT+0330',
310
        'gmt+430': 'GMT+0430', 'gmt+0430': 'GMT+0430',
311
        'gmt+530': 'GMT+0530', 'gmt+0530': 'GMT+0530',
312
        'gmt+630': 'GMT+0630', 'gmt+0630': 'GMT+0630',
313
        'gmt+730': 'GMT+0730', 'gmt+0730': 'GMT+0730',
314
        'gmt+830': 'GMT+0830', 'gmt+0830': 'GMT+0830',
315
        'gmt+930': 'GMT+0930', 'gmt+0930': 'GMT+0930',
316
        'gmt+1030': 'GMT+1030',
317
        'gmt+1130': 'GMT+1130',
318
        'gmt+1230': 'GMT+1230',
319

320
        'gmt-130': 'GMT-0130', 'gmt-0130': 'GMT-0130',
321
        'gmt-230': 'GMT-0230', 'gmt-0230': 'GMT-0230',
322
        'gmt-330': 'GMT-0330', 'gmt-0330': 'GMT-0330',
323
        'gmt-430': 'GMT-0430', 'gmt-0430': 'GMT-0430',
324
        'gmt-530': 'GMT-0530', 'gmt-0530': 'GMT-0530',
325
        'gmt-630': 'GMT-0630', 'gmt-0630': 'GMT-0630',
326
        'gmt-730': 'GMT-0730', 'gmt-0730': 'GMT-0730',
327
        'gmt-830': 'GMT-0830', 'gmt-0830': 'GMT-0830',
328
        'gmt-930': 'GMT-0930', 'gmt-0930': 'GMT-0930',
329
        'gmt-1030': 'GMT-1030',
330
        'gmt-1130': 'GMT-1130',
331
        'gmt-1230': 'GMT-1230',
332

333
        'greenwich': 'Greenwich', 'hongkong': 'Hongkong',
334
        'iceland': 'Iceland', 'iran': 'Iran', 'israel': 'Israel',
335
        'jamaica': 'Jamaica', 'japan': 'Japan',
336
        'mexico/bajanorte': 'Mexico/BajaNorte',
337
        'mexico/bajasur': 'Mexico/BajaSur', 'mexico/general': 'Mexico/General',
338
        'mst': 'US/Mountain', 'pst': 'US/Pacific', 'poland': 'Poland',
339
        'singapore': 'Singapore', 'turkey': 'Turkey', 'universal': 'Universal',
340
        'utc': 'Universal', 'uct': 'Universal', 'us/alaska': 'US/Alaska',
341
        'us/aleutian': 'US/Aleutian', 'us/arizona': 'US/Arizona',
342
        'us/central': 'US/Central', 'us/eastern': 'US/Eastern',
343
        'us/east-indiana': 'US/East-Indiana', 'us/hawaii': 'US/Hawaii',
344
        'us/indiana-starke': 'US/Indiana-Starke', 'us/michigan': 'US/Michigan',
345
        'us/mountain': 'US/Mountain', 'us/pacific': 'US/Pacific',
346
        'us/samoa': 'US/Samoa',
347

348
        'ut': 'Universal',
349
        'bst': 'GMT+1', 'mest': 'GMT+2', 'sst': 'GMT+2',
350
        'fst': 'GMT+2', 'wadt': 'GMT+8', 'eadt': 'GMT+11', 'nzdt': 'GMT+13',
351
        'wet': 'GMT', 'wat': 'GMT-1', 'at': 'GMT-2', 'ast': 'GMT-4',
352
        'nt': 'GMT-11', 'idlw': 'GMT-12', 'cet': 'GMT+1', 'cest': 'GMT+2',
353
        'met': 'GMT+1',
354
        'mewt': 'GMT+1', 'swt': 'GMT+1', 'fwt': 'GMT+1', 'eet': 'GMT+2',
355
        'eest': 'GMT+3',
356
        'bt': 'GMT+3', 'zp4': 'GMT+4', 'zp5': 'GMT+5', 'zp6': 'GMT+6',
357
        'wast': 'GMT+7', 'cct': 'GMT+8', 'jst': 'GMT+9', 'east': 'GMT+10',
358
        'gst': 'GMT+10', 'nzt': 'GMT+12', 'nzst': 'GMT+12', 'idle': 'GMT+12',
359
        'ret': 'GMT+4'
360
    }
361

362
    def __init__(self):
1✔
363
        self._db = _data
1✔
364
        self._d, self._zidx = {}, self._zmap.keys()
1✔
365

366
    def __getitem__(self, k):
1✔
367
        try:
1✔
368
            n = self._zmap[k.lower()]
1✔
369
        except KeyError:
1✔
370
            if numericTimeZoneMatch(k) is None:
1✔
371
                raise DateTimeError('Unrecognized timezone: %s' % k)
1✔
372
            return k
1✔
373
        try:
1✔
374
            return self._d[n]
1✔
375
        except KeyError:
1✔
376
            z = self._d[n] = _timezone(self._db[n])
1✔
377
            return z
1✔
378

379

380
def _findLocalTimeZoneName(isDST):
1✔
381
    if not _time.daylight:  # pragma: no cover
382
        # Daylight savings does not occur in this time zone.
383
        isDST = 0
384
    try:
1✔
385
        # Get the name of the current time zone depending
386
        # on DST.
387
        _localzone = _cache._zmap[tzname[isDST].lower()]
1✔
388
    except KeyError:
1✔
389
        try:
1✔
390
            # Generate a GMT-offset zone name.
391
            localzone = _time.altzone if isDST else _time.timezone
1✔
392

393
            offset = (-localzone / (60 * 60))
1✔
394
            majorOffset = int(offset)
1✔
395
            minorOffset = 0
1✔
396
            if majorOffset != 0:  # pragma: no cover
397
                minorOffset = abs(int((offset % majorOffset) * 60.0))
398
            m = '+' if majorOffset >= 0 else ''
1✔
399
            lz = '%s%0.02d%0.02d' % (m, majorOffset, minorOffset)
1✔
400
            _localzone = _cache._zmap[('GMT%s' % lz).lower()]
1✔
401
        except Exception:
1✔
402
            _localzone = ''
1✔
403
    return _localzone
1✔
404

405
# Some utility functions for calculating dates:
406

407

408
def _calcSD(t):
1✔
409
    # Returns timezone-independent days since epoch and the fractional
410
    # part of the days.
411
    dd = t + EPOCH - 86400.0
1✔
412
    d = dd / 86400.0
1✔
413
    s = d - math.floor(d)
1✔
414
    return s, d
1✔
415

416

417
def _calcDependentSecond(tz, t):
1✔
418
    # Calculates the timezone-dependent second (integer part only)
419
    # from the timezone-independent second.
420
    fset = _tzoffset(tz, t)
1✔
421
    return fset + int(math.floor(t)) + EPOCH - 86400
1✔
422

423

424
def _calcDependentSecond2(yr, mo, dy, hr, mn, sc):
1✔
425
    # Calculates the timezone-dependent second (integer part only)
426
    # from the date given.
427
    ss = int(hr) * 3600 + int(mn) * 60 + int(sc)
1✔
428
    x = int(_julianday(yr, mo, dy) - jd1901) * 86400 + ss
1✔
429
    return x
1✔
430

431

432
def _calcIndependentSecondEtc(tz, x, ms):
1✔
433
    # Derive the timezone-independent second from the timezone
434
    # dependent second.
435
    fsetAtEpoch = _tzoffset(tz, 0.0)
1✔
436
    nearTime = x - fsetAtEpoch - EPOCH + 86400 + ms
1✔
437
    # nearTime is now within an hour of being correct.
438
    # Recalculate t according to DST.
439
    fset = _tzoffset(tz, nearTime)
1✔
440
    x_adjusted = x - fset + ms
1✔
441
    d = x_adjusted / 86400.0
1✔
442
    t = x_adjusted - EPOCH + 86400
1✔
443
    millis = (x + 86400 - fset) * 1000 + ms * 1000 - EPOCH * 1000
1✔
444
    s = d - math.floor(d)
1✔
445
    return s, d, t, millis
1✔
446

447

448
def _calcHMS(x, ms):
1✔
449
    # hours, minutes, seconds from integer and float.
450
    hr = x // 3600
1✔
451
    x = x - hr * 3600
1✔
452
    mn = x // 60
1✔
453
    sc = x - mn * 60 + ms
1✔
454
    return hr, mn, sc
1✔
455

456

457
def _julianday(y, m, d):
1✔
458
    if m > 12:
1✔
459
        y = y + m // 12
1✔
460
        m = m % 12
1✔
461
    elif m < 1:
1✔
462
        m = -m
1✔
463
        y = y - m // 12 - 1
1✔
464
        m = 12 - m % 12
1✔
465
    if y > 0:
1✔
466
        yr_correct = 0
1✔
467
    else:
468
        yr_correct = 3
1✔
469
    if m < 3:
1✔
470
        y, m = y - 1, m + 12
1✔
471
    if y * 10000 + m * 100 + d > 15821014:
1✔
472
        b = 2 - y // 100 + y // 400
1✔
473
    else:
474
        b = 0
1✔
475
    return (1461 * y - yr_correct) // 4 + 306001 * \
1✔
476
        (m + 1) // 10000 + d + 1720994 + b
477

478

479
def _tzoffset(tz, t):
1✔
480
    try:
1✔
481
        return DateTimeParser._tzinfo[tz].info(t)[0]
1✔
482
    except Exception:
1✔
483
        if numericTimeZoneMatch(tz) is not None:
1✔
484
            offset = int(tz[1:3]) * 3600 + int(tz[3:5]) * 60
1✔
485
            if tz[0] == '-':
1✔
486
                return -offset
1✔
487
            return offset
1✔
488
        else:
489
            return 0  # Assume UTC
1✔
490

491

492
def _correctYear(year):
1✔
493
    # Y2K patch.
494
    if year >= 0 and year < 100:
1✔
495
        # 00-69 means 2000-2069, 70-99 means 1970-1999.
496
        if year < 70:
1✔
497
            year = 2000 + year
1✔
498
        else:
499
            year = 1900 + year
1✔
500
    return year
1✔
501

502

503
def safegmtime(t):
1✔
504
    '''gmtime with a safety zone.'''
505
    try:
1✔
506
        return _time.gmtime(t)
1✔
507
    except (ValueError, OverflowError):  # Py2/Py3 respectively
1✔
508
        raise TimeError('The time %r is beyond the range '
1✔
509
                        'of this Python implementation.' % t)
510

511

512
def safelocaltime(t):
1✔
513
    '''localtime with a safety zone.'''
514
    try:
1✔
515
        return _time.localtime(t)
1✔
516
    except (ValueError, OverflowError):  # Py2/Py3 respectively
1✔
517
        raise TimeError('The time %r is beyond the range '
1✔
518
                        'of this Python implementation.' % t)
519

520

521
class DateTimeParser:
1✔
522

523
    def parse(self, arg, local=True):
1✔
524
        """
525
        Parse a string containing some sort of date-time data into a tuple.
526

527
        As a general rule, any date-time representation that is
528
        recognized and unambigous to a resident of North America is
529
        acceptable.(The reason for this qualification is that
530
        in North America, a date like: 2/1/1994 is interpreted
531
        as February 1, 1994, while in some parts of the world,
532
        it is interpreted as January 2, 1994.) A date/time
533
        string consists of two components, a date component and
534
        an optional time component, separated by one or more
535
        spaces. If the time component is omited, 12:00am is
536
        assumed. Any recognized timezone name specified as the
537
        final element of the date/time string will be used for
538
        computing the date/time value. (If you create a DateTime
539
        with the string 'Mar 9, 1997 1:45pm US/Pacific', the
540
        value will essentially be the same as if you had captured
541
        time.time() at the specified date and time on a machine in
542
        that timezone)::
543

544
            x = parse('1997/3/9 1:45pm')
545
            # returns specified time, represented in local machine zone.
546

547
            y = parse('Mar 9, 1997 13:45:00')
548
            # y is equal to x
549

550
        The function automatically detects and handles `ISO8601
551
        compliant dates <http://www.w3.org/TR/NOTE-datetime>`_
552
        (YYYY-MM-DDThh:ss:mmTZD).
553

554
        The date component consists of year, month, and day
555
        values. The year value must be a one-, two-, or
556
        four-digit integer. If a one- or two-digit year is
557
        used, the year is assumed to be in the twentieth
558
        century. The month may an integer, from 1 to 12, a
559
        month name, or a month abreviation, where a period may
560
        optionally follow the abreviation. The day must be an
561
        integer from 1 to the number of days in the month. The
562
        year, month, and day values may be separated by
563
        periods, hyphens, forward, shashes, or spaces. Extra
564
        spaces are permitted around the delimiters. Year,
565
        month, and day values may be given in any order as long
566
        as it is possible to distinguish the components. If all
567
        three components are numbers that are less than 13,
568
        then a a month-day-year ordering is assumed.
569

570
        The time component consists of hour, minute, and second
571
        values separated by colons.  The hour value must be an
572
        integer between 0 and 23 inclusively. The minute value
573
        must be an integer between 0 and 59 inclusively. The
574
        second value may be an integer value between 0 and
575
        59.999 inclusively. The second value or both the minute
576
        and second values may be ommitted. The time may be
577
        followed by am or pm in upper or lower case, in which
578
        case a 12-hour clock is assumed.
579

580
        :keyword bool local: If no timezone can be parsed from the string,
581
            figure out what timezone it is in the local area
582
            on the given date and return that.
583

584
        :return: This function returns a tuple (year, month, day, hour, minute,
585
            second, timezone_string).
586

587
        :raises SyntaxError:
588
            If a string argument passed to the DateTime constructor cannot be
589
            parsed (for example, it is empty).
590
        :raises DateError:
591
            If the date components are invalid.
592
        :raises DateTimeError:
593
           If the time or timezone components are invalid.
594
        :raises TypeError:
595
           If the argument is not a string.
596
        """
597
        if not isinstance(arg, StringTypes):
1✔
598
            raise TypeError('Expected a string argument')
1✔
599

600
        if not arg:
1✔
601
            raise SyntaxError(arg)
1✔
602

603
        if arg.find(' ') == -1 and len(arg) >= 5 and arg[4] == '-':
1✔
604
            yr, mo, dy, hr, mn, sc, tz = self._parse_iso8601(arg)
1✔
605
        else:
606
            yr, mo, dy, hr, mn, sc, tz = self._parse(arg, local)
1✔
607

608
        if not self._validDate(yr, mo, dy):
1✔
609
            raise DateError(arg, yr, mo, dy)
1✔
610
        if not self._validTime(hr, mn, int(sc)):
1✔
611
            raise TimeError(arg)
1✔
612

613
        return yr, mo, dy, hr, mn, sc, tz
1✔
614

615
    def time(self, arg):
1✔
616
        """
617
        Parse a string containing some sort of date-time data and return
618
        the value in seconds since the Epoch.
619

620
        :return: A floating point number representing the time in
621
            seconds since the Epoch (in UTC).
622

623
        :raises DateTimeError: If a timezone was parsed from the argument,
624
           but it wasn't a known named or numeric timezone.
625

626
        .. seealso:: :meth:`parse` for the description of allowed input values.
627
        """
628

629
        yr, mo, dy, hr, mn, sc, tz = self.parse(arg)
1✔
630

631
        ms = sc - math.floor(sc)
1✔
632
        x = _calcDependentSecond2(yr, mo, dy, hr, mn, sc)
1✔
633

634
        if tz:
1✔
635
            try:
1✔
636
                tz = self._tzinfo._zmap[tz.lower()]
1✔
637
            except KeyError:
1✔
638
                if numericTimeZoneMatch(tz) is None:
1✔
639
                    raise DateTimeError('Unknown time zone in date: %s' % arg)
1✔
640
        else:
641
            tz = self._calcTimezoneName(x, ms)
1✔
642

643
        _s, _d, t, _millisecs = _calcIndependentSecondEtc(tz, x, ms)
1✔
644
        return t
1✔
645

646
    int_pattern = re.compile(r'([0-9]+)')  # AJ
1✔
647
    flt_pattern = re.compile(r':([0-9]+\.[0-9]+)')  # AJ
1✔
648
    name_pattern = re.compile(r'([a-zA-Z]+)', re.I)  # AJ
1✔
649
    space_chars = ' \t\n'
1✔
650
    delimiters = '-/.:,+'
1✔
651
    _month_len = (
1✔
652
        (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31),
653
        (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
654
    )
655
    _until_month = (
1✔
656
        (0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334),
657
        (0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335)
658
    )
659
    _monthmap = {
1✔
660
        'january': 1, 'jan': 1,
661
        'february': 2, 'feb': 2,
662
        'march': 3, 'mar': 3,
663
        'april': 4, 'apr': 4,
664
        'may': 5,
665
        'june': 6, 'jun': 6,
666
        'july': 7, 'jul': 7,
667
        'august': 8, 'aug': 8,
668
        'september': 9, 'sep': 9, 'sept': 9,
669
        'october': 10, 'oct': 10,
670
        'november': 11, 'nov': 11,
671
        'december': 12, 'dec': 12
672
    }
673
    _daymap = {
1✔
674
        'sunday': 1, 'sun': 1,
675
        'monday': 2, 'mon': 2,
676
        'tuesday': 3, 'tues': 3, 'tue': 3,
677
        'wednesday': 4, 'wed': 4,
678
        'thursday': 5, 'thurs': 5, 'thur': 5, 'thu': 5,
679
        'friday': 6, 'fri': 6,
680
        'saturday': 7, 'sat': 7
681
    }
682

683
    _localzone0 = _findLocalTimeZoneName(0)
1✔
684
    _localzone1 = _findLocalTimeZoneName(1)
1✔
685
    _multipleZones = (_localzone0 != _localzone1)
1✔
686
    # For backward compatibility only:
687
    _isDST = _time.localtime()[8]
1✔
688
    _localzone = _localzone1 if _isDST else _localzone0
1✔
689

690
    _tzinfo = _cache()
1✔
691

692
    def localZone(self, ltm=None):
1✔
693
        '''Returns the time zone on the given date.  The time zone
694
        can change according to daylight savings.'''
695
        if not self._multipleZones:
1✔
696
            return self._localzone0
1✔
697

698
        ltm = _time.localtime() if ltm is None else ltm
1✔
699
        isDST = ltm[8]
1✔
700
        lz = self._localzone1 if isDST else self._localzone0
1✔
701
        return lz
1✔
702

703
    def _calcTimezoneName(self, x, ms):
1✔
704
        # Derive the name of the local time zone at the given
705
        # timezone-dependent second.
706
        if not self._multipleZones:
1✔
707
            return self._localzone0
1✔
708
        fsetAtEpoch = _tzoffset(self._localzone0, 0.0)
1✔
709
        nearTime = x - fsetAtEpoch - EPOCH + 86400 + ms
1✔
710
        # nearTime is within an hour of being correct.
711
        ltm = safelocaltime(nearTime)
1✔
712
        tz = self.localZone(ltm)
1✔
713
        return tz
1✔
714

715
    def _parse(self, string, local=True):
1✔
716
        # Parse date-time components from a string
717
        month = year = tz = tm = None
1✔
718
        spaces = self.space_chars
1✔
719
        intpat = self.int_pattern
1✔
720
        fltpat = self.flt_pattern
1✔
721
        wordpat = self.name_pattern
1✔
722
        delimiters = self.delimiters
1✔
723
        MonthNumbers = self._monthmap
1✔
724
        DayOfWeekNames = self._daymap
1✔
725
        ValidZones = self._tzinfo._zidx
1✔
726
        TimeModifiers = ['am', 'pm']
1✔
727

728
        string = string.strip()
1✔
729

730
        # Find timezone first, since it should always be the last
731
        # element, and may contain a slash, confusing the parser.
732

733
        # First check for time zone of form +dd:dd
734
        tz = _iso_tz_re.search(string)
1✔
735
        if tz:
1✔
736
            tz = tz.start(0)
1✔
737
            tz, string = string[tz:], string[:tz].strip()
1✔
738
            tz = tz[:3] + tz[4:]
1✔
739
        else:
740
            # Look at last token
741
            sp = string.split()
1✔
742
            tz = sp[-1]
1✔
743
            if tz and (tz.lower() in ValidZones):
1✔
744
                string = ' '.join(sp[:-1])
1✔
745
            else:
746
                tz = None  # Decide later, since the default time zone
1✔
747
                # could depend on the date.
748

749
        ints = []
1✔
750
        i, len_string = 0, len(string)
1✔
751
        while i < len_string:
1✔
752
            while i < len_string and string[i] in spaces:
1✔
753
                i = i + 1
1✔
754
            if i < len_string and string[i] in delimiters:
1✔
755
                d = string[i]
1✔
756
                i = i + 1
1✔
757
            else:
758
                d = ''
1✔
759
            while i < len_string and string[i] in spaces:
1✔
760
                i = i + 1
1✔
761

762
            # The float pattern needs to look back 1 character, because it
763
            # actually looks for a preceding colon like ':33.33'. This is
764
            # needed to avoid accidentally matching the date part of a
765
            # dot-separated date string such as '1999.12.31'.
766
            if i > 0:
1✔
767
                b = i - 1
1✔
768
            else:
769
                b = i
1✔
770

771
            ts_results = fltpat.match(string, b)
1✔
772
            if ts_results:
1✔
773
                s = ts_results.group(1)
1✔
774
                i = i + len(s)
1✔
775
                ints.append(float(s))
1✔
776
                continue
1✔
777

778
            # AJ
779
            ts_results = intpat.match(string, i)
1✔
780
            if ts_results:
1✔
781
                s = ts_results.group(0)
1✔
782

783
                ls = len(s)
1✔
784
                i = i + ls
1✔
785
                if (ls == 4 and d and d in '+-' and
1✔
786
                        (len(ints) + (not not month) >= 3)):
787
                    tz = '{}{}'.format(d, s)
1✔
788
                else:
789
                    v = int(s)
1✔
790
                    ints.append(v)
1✔
791
                continue
1✔
792

793
            ts_results = wordpat.match(string, i)
1✔
794
            if ts_results:
1✔
795
                o = ts_results.group(0)
1✔
796
                s = o.lower()
1✔
797
                i = i + len(s)
1✔
798
                if i < len_string and string[i] == '.':
1✔
799
                    i = i + 1
1✔
800
                # Check for month name:
801
                if s in MonthNumbers:
1✔
802
                    v = MonthNumbers[s]
1✔
803
                    if month is None:
1✔
804
                        month = v
1✔
805
                    else:
806
                        raise SyntaxError(string)
1✔
807
                    continue  # pragma: no cover
808
                # Check for time modifier:
809
                if s in TimeModifiers:
1✔
810
                    if tm is None:
1✔
811
                        tm = s
1✔
812
                    else:
813
                        raise SyntaxError(string)
1✔
814
                    continue  # pragma: no cover
815
                # Check for and skip day of week:
816
                if s in DayOfWeekNames:
1!
817
                    continue
1✔
818
            raise SyntaxError(string)
1✔
819

820
        day = None
1✔
821
        if ints[-1] > 60 and d not in ['.', ':'] and len(ints) > 2:
1✔
822
            year = ints[-1]
1✔
823
            del ints[-1]
1✔
824
            if month:
1✔
825
                day = ints[0]
1✔
826
                del ints[:1]
1✔
827
            else:
828
                month = ints[0]
1✔
829
                day = ints[1]
1✔
830
                del ints[:2]
1✔
831
        elif month:
1✔
832
            if len(ints) > 1:
1!
833
                if ints[0] > 31:
1✔
834
                    year = ints[0]
1✔
835
                    day = ints[1]
1✔
836
                else:
837
                    year = ints[1]
1✔
838
                    day = ints[0]
1✔
839
                del ints[:2]
1✔
840
        elif len(ints) > 2:
1✔
841
            if ints[0] > 31:
1✔
842
                year = ints[0]
1✔
843
                if ints[1] > 12:
1✔
844
                    day = ints[1]
1✔
845
                    month = ints[2]
1✔
846
                else:
847
                    day = ints[2]
1✔
848
                    month = ints[1]
1✔
849
            if ints[1] > 31:
1✔
850
                year = ints[1]
1✔
851
                if ints[0] > 12 and ints[2] <= 12:
1✔
852
                    day = ints[0]
1✔
853
                    month = ints[2]
1✔
854
                elif ints[2] > 12 and ints[0] <= 12:
1✔
855
                    day = ints[2]
1✔
856
                    month = ints[0]
1✔
857
            elif ints[2] > 31:
1✔
858
                year = ints[2]
1✔
859
                if ints[0] > 12:
1✔
860
                    day = ints[0]
1✔
861
                    month = ints[1]
1✔
862
                else:
863
                    day = ints[1]
1✔
864
                    month = ints[0]
1✔
865
            elif ints[0] <= 12:
1✔
866
                month = ints[0]
1✔
867
                day = ints[1]
1✔
868
                year = ints[2]
1✔
869
            del ints[:3]
1✔
870

871
        if day is None:
1✔
872
            # Use today's date.
873
            year, month, day = _time.localtime()[:3]
1✔
874

875
        year = _correctYear(year)
1✔
876
        if year < 1000:
1✔
877
            raise SyntaxError(string)
1✔
878

879
        leap = year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
1✔
880
        try:
1✔
881
            if not day or day > self._month_len[leap][month]:
1✔
882
                raise DateError(string)
1✔
883
        except IndexError:
1✔
884
            raise DateError(string)
1✔
885
        tod = 0
1✔
886
        if ints:
1✔
887
            i = ints[0]
1✔
888
            # Modify hour to reflect am/pm
889
            if tm and (tm == 'pm') and i < 12:
1✔
890
                i = i + 12
1✔
891
            if tm and (tm == 'am') and i == 12:
1✔
892
                i = 0
1✔
893
            if i > 24:
1✔
894
                raise DateTimeError(string)
1✔
895
            tod = tod + int(i) * 3600
1✔
896
            del ints[0]
1✔
897
            if ints:
1✔
898
                i = ints[0]
1✔
899
                if i > 60:
1✔
900
                    raise DateTimeError(string)
1✔
901
                tod = tod + int(i) * 60
1✔
902
                del ints[0]
1✔
903
                if ints:
1✔
904
                    i = ints[0]
1✔
905
                    if i > 60:
1✔
906
                        raise DateTimeError(string)
1✔
907
                    tod = tod + i
1✔
908
                    del ints[0]
1✔
909
                    if ints:
1✔
910
                        raise SyntaxError(string)
1✔
911

912
        tod_int = int(math.floor(tod))
1✔
913
        ms = tod - tod_int
1✔
914
        hr, mn, sc = _calcHMS(tod_int, ms)
1✔
915

916
        if local and not tz:
1✔
917
            # Figure out what time zone it is in the local area
918
            # on the given date.
919
            x = _calcDependentSecond2(year, month, day, hr, mn, sc)
1✔
920
            tz = self._calcTimezoneName(x, ms)
1✔
921

922
        return year, month, day, hr, mn, sc, tz
1✔
923

924
    def _validDate(self, y, m, d):
1✔
925
        if m < 1 or m > 12 or y < 0 or d < 1 or d > 31:
1✔
926
            return 0
1✔
927
        is_leap_year = y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)
1✔
928
        return d <= self._month_len[is_leap_year][m]
1✔
929

930
    def _validTime(self, h, m, s):
1✔
931
        return h >= 0 and h <= 23 and m >= 0 and m <= 59 and s >= 0 and s < 60
1✔
932

933
    def _parse_iso8601(self, s):
1✔
934
        try:
1✔
935
            return self.__parse_iso8601(s)
1✔
936
        except IndexError:
1✔
937
            raise DateError(
1✔
938
                'Not an ISO 8601 compliant date string: "%s"' % s)
939

940
    def __parse_iso8601(self, s):
1✔
941
        """Parse an ISO 8601 compliant date.
942

943
        TODO: Not all allowed formats are recognized (for some examples see
944
        http://www.cl.cam.ac.uk/~mgk25/iso-time.html).
945
        """
946
        year = 0
1✔
947
        month = day = 1
1✔
948
        hour = minute = seconds = hour_off = min_off = 0
1✔
949
        tzsign = '+'
1✔
950

951
        datereg = re.compile(
1✔
952
            '([0-9]{4})(-([0-9][0-9]))?(-([0-9][0-9]))?')
953
        timereg = re.compile(
1✔
954
            r'([0-9]{2})(:([0-9][0-9]))?(:([0-9][0-9]))?(\.[0-9]{1,20})?'
955
            r'(\s*([-+])([0-9]{2})(:?([0-9]{2}))?)?')
956

957
        # Date part
958

959
        fields = datereg.split(s.strip())
1✔
960
        if fields[1]:
1!
961
            year = int(fields[1])
1✔
962
        if fields[3]:
1!
963
            month = int(fields[3])
1✔
964
        if fields[5]:
1!
965
            day = int(fields[5])
1✔
966

967
        if s.find('T') > -1:
1✔
968
            fields = timereg.split(s[s.find('T') + 1:])
1✔
969

970
            if fields[1]:
1!
971
                hour = int(fields[1])
1✔
972
            if fields[3]:
1!
973
                minute = int(fields[3])
1✔
974
            if fields[5]:
1✔
975
                seconds = int(fields[5])
1✔
976
            if fields[6]:
1✔
977
                seconds = seconds + float(fields[6])
1✔
978

979
            if fields[8]:
1✔
980
                tzsign = fields[8]
1✔
981
            if fields[9]:
1✔
982
                hour_off = int(fields[9])
1✔
983
            if fields[11]:
1✔
984
                min_off = int(fields[11])
1✔
985

986
        return (year, month, day, hour, minute, seconds,
1✔
987
                '%s%02d%02d' % (tzsign, hour_off, min_off))
988

989

990
parser = DateTimeParser()
1✔
991
parse = parser.parse
1✔
992
time = parser.time
1✔
993

994
######################################################################
995
# Time-zone info based soley on offsets
996
#
997
# Share tzinfos for the same offset
998

999

1000
class _tzinfo(_std_tzinfo):
1✔
1001

1002
    def __init__(self, minutes):
1✔
1003
        if abs(minutes) > 1439:
1✔
1004
            raise ValueError("Time-zone offset is too large,", minutes)
1✔
1005
        self.__minutes = minutes
1✔
1006
        self.__offset = _timedelta(minutes=minutes)
1✔
1007

1008
    def utcoffset(self, dt):
1✔
1009
        return self.__offset
1✔
1010

1011
    def __reduce__(self):
1✔
1012
        return tzinfo, (self.__minutes, )
1✔
1013

1014
    def dst(self, dt):
1✔
1015
        return None
1✔
1016

1017
    def tzname(self, dt):
1✔
1018
        return None
1✔
1019

1020
    def __repr__(self):  # pragma: no cover
1021
        return 'tzinfo(%d)' % self.__minutes
1022

1023

1024
_tzinfos = {}
1✔
1025

1026

1027
def tzinfo(offset):
1✔
1028

1029
    info = _tzinfos.get(offset)
1✔
1030
    if info is None:
1✔
1031
        # We haven't seen this one before. we need to save it.
1032

1033
        # Use setdefault to avoid a race condition and make sure we have
1034
        # only one
1035
        info = _tzinfos.setdefault(offset, _tzinfo(offset))
1✔
1036

1037
    return info
1✔
1038

1039

1040
tzinfo.__safe_for_unpickling__ = True
1✔
1041

1042
#
1043
######################################################################
1044

1045

1046
def parseDatetimetz(string, local=True):
1✔
1047
    """Parse the given string using :func:`parse`.
1048

1049
    Return a :class:`datetime.datetime` instance.
1050
    """
1051
    y, mo, d, h, m, s, tz = parse(string, local)
1✔
1052
    s, micro = divmod(s, 1.0)
1✔
1053
    micro = round(micro * 1000000)
1✔
1054
    if tz:
1✔
1055
        offset = _tzoffset(tz, None) / 60
1✔
1056
        _tzinfo = tzinfo(offset)
1✔
1057
    else:
1058
        _tzinfo = None
1✔
1059
    return _datetime(y, mo, d, int(h), int(m), int(s), int(micro), _tzinfo)
1✔
1060

1061

1062
_iso_tz_re = re.compile(r"[-+]\d\d:\d\d$")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc