• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Strptime-related classes and functions.
2
3CLASSES:
4    LocaleTime -- Discovers and stores locale-specific time information
5    TimeRE -- Creates regexes for pattern matching a string of text containing
6                time information
7
8FUNCTIONS:
9    _getlang -- Figure out what language is being used for the locale
10    strptime -- Calculates the time struct represented by the passed-in string
11
12"""
13import os
14import time
15import locale
16import calendar
17from re import compile as re_compile
18from re import sub as re_sub
19from re import IGNORECASE
20from re import escape as re_escape
21from datetime import (date as datetime_date,
22                      timedelta as datetime_timedelta,
23                      timezone as datetime_timezone)
24from _thread import allocate_lock as _thread_allocate_lock
25
26__all__ = []
27
28def _getlang():
29    # Figure out what the current language is set to.
30    return locale.getlocale(locale.LC_TIME)
31
32def _findall(haystack, needle):
33    # Find all positions of needle in haystack.
34    if not needle:
35        return
36    i = 0
37    while True:
38        i = haystack.find(needle, i)
39        if i < 0:
40            break
41        yield i
42        i += len(needle)
43
44class LocaleTime(object):
45    """Stores and handles locale-specific information related to time.
46
47    ATTRIBUTES:
48        f_weekday -- full weekday names (7-item list)
49        a_weekday -- abbreviated weekday names (7-item list)
50        f_month -- full month names (13-item list; dummy value in [0], which
51                    is added by code)
52        a_month -- abbreviated month names (13-item list, dummy value in
53                    [0], which is added by code)
54        am_pm -- AM/PM representation (2-item list)
55        LC_date_time -- format string for date/time representation (string)
56        LC_date -- format string for date representation (string)
57        LC_time -- format string for time representation (string)
58        timezone -- daylight- and non-daylight-savings timezone representation
59                    (2-item list of sets)
60        lang -- Language used by instance (2-item tuple)
61    """
62
63    def __init__(self):
64        """Set all attributes.
65
66        Order of methods called matters for dependency reasons.
67
68        The locale language is set at the offset and then checked again before
69        exiting.  This is to make sure that the attributes were not set with a
70        mix of information from more than one locale.  This would most likely
71        happen when using threads where one thread calls a locale-dependent
72        function while another thread changes the locale while the function in
73        the other thread is still running.  Proper coding would call for
74        locks to prevent changing the locale while locale-dependent code is
75        running.  The check here is done in case someone does not think about
76        doing this.
77
78        Only other possible issue is if someone changed the timezone and did
79        not call tz.tzset .  That is an issue for the programmer, though,
80        since changing the timezone is worthless without that call.
81
82        """
83        self.lang = _getlang()
84        self.__calc_weekday()
85        self.__calc_month()
86        self.__calc_am_pm()
87        self.__calc_timezone()
88        self.__calc_date_time()
89        if _getlang() != self.lang:
90            raise ValueError("locale changed during initialization")
91        if time.tzname != self.tzname or time.daylight != self.daylight:
92            raise ValueError("timezone changed during initialization")
93
94    def __calc_weekday(self):
95        # Set self.a_weekday and self.f_weekday using the calendar
96        # module.
97        a_weekday = [calendar.day_abbr[i].lower() for i in range(7)]
98        f_weekday = [calendar.day_name[i].lower() for i in range(7)]
99        self.a_weekday = a_weekday
100        self.f_weekday = f_weekday
101
102    def __calc_month(self):
103        # Set self.f_month and self.a_month using the calendar module.
104        a_month = [calendar.month_abbr[i].lower() for i in range(13)]
105        f_month = [calendar.month_name[i].lower() for i in range(13)]
106        self.a_month = a_month
107        self.f_month = f_month
108
109    def __calc_am_pm(self):
110        # Set self.am_pm by using time.strftime().
111
112        # The magic date (1999,3,17,hour,44,55,2,76,0) is not really that
113        # magical; just happened to have used it everywhere else where a
114        # static date was needed.
115        am_pm = []
116        for hour in (1, 22):
117            time_tuple = time.struct_time((1999,3,17,hour,44,55,2,76,0))
118            # br_FR has AM/PM info (' ',' ').
119            am_pm.append(time.strftime("%p", time_tuple).lower().strip())
120        self.am_pm = am_pm
121
122    def __calc_date_time(self):
123        # Set self.date_time, self.date, & self.time by using
124        # time.strftime().
125
126        # Use (1999,3,17,22,44,55,2,76,0) for magic date because the amount of
127        # overloaded numbers is minimized.  The order in which searches for
128        # values within the format string is very important; it eliminates
129        # possible ambiguity for what something represents.
130        time_tuple = time.struct_time((1999,3,17,22,44,55,2,76,0))
131        time_tuple2 = time.struct_time((1999,1,3,1,1,1,6,3,0))
132        replacement_pairs = [
133            ('1999', '%Y'), ('99', '%y'), ('22', '%H'),
134            ('44', '%M'), ('55', '%S'), ('76', '%j'),
135            ('17', '%d'), ('03', '%m'), ('3', '%m'),
136            # '3' needed for when no leading zero.
137            ('2', '%w'), ('10', '%I'),
138            # Non-ASCII digits
139            ('\u0661\u0669\u0669\u0669', '%Y'),
140            ('\u0669\u0669', '%Oy'),
141            ('\u0662\u0662', '%OH'),
142            ('\u0664\u0664', '%OM'),
143            ('\u0665\u0665', '%OS'),
144            ('\u0661\u0667', '%Od'),
145            ('\u0660\u0663', '%Om'),
146            ('\u0663', '%Om'),
147            ('\u0662', '%Ow'),
148            ('\u0661\u0660', '%OI'),
149        ]
150        date_time = []
151        for directive in ('%c', '%x', '%X'):
152            current_format = time.strftime(directive, time_tuple).lower()
153            current_format = current_format.replace('%', '%%')
154            # The month and the day of the week formats are treated specially
155            # because of a possible ambiguity in some locales where the full
156            # and abbreviated names are equal or names of different types
157            # are equal. See doc of __find_month_format for more details.
158            lst, fmt = self.__find_weekday_format(directive)
159            if lst:
160                current_format = current_format.replace(lst[2], fmt, 1)
161            lst, fmt = self.__find_month_format(directive)
162            if lst:
163                current_format = current_format.replace(lst[3], fmt, 1)
164            if self.am_pm[1]:
165                # Must deal with possible lack of locale info
166                # manifesting itself as the empty string (e.g., Swedish's
167                # lack of AM/PM info) or a platform returning a tuple of empty
168                # strings (e.g., MacOS 9 having timezone as ('','')).
169                current_format = current_format.replace(self.am_pm[1], '%p')
170            for tz_values in self.timezone:
171                for tz in tz_values:
172                    if tz:
173                        current_format = current_format.replace(tz, "%Z")
174            # Transform all non-ASCII digits to digits in range U+0660 to U+0669.
175            current_format = re_sub(r'\d(?<![0-9])',
176                                    lambda m: chr(0x0660 + int(m[0])),
177                                    current_format)
178            for old, new in replacement_pairs:
179                current_format = current_format.replace(old, new)
180            # If %W is used, then Sunday, 2005-01-03 will fall on week 0 since
181            # 2005-01-03 occurs before the first Monday of the year.  Otherwise
182            # %U is used.
183            if '00' in time.strftime(directive, time_tuple2):
184                U_W = '%W'
185            else:
186                U_W = '%U'
187            current_format = current_format.replace('11', U_W)
188            date_time.append(current_format)
189        self.LC_date_time = date_time[0]
190        self.LC_date = date_time[1]
191        self.LC_time = date_time[2]
192
193    def __find_month_format(self, directive):
194        """Find the month format appropriate for the current locale.
195
196        In some locales (for example French and Hebrew), the default month
197        used in __calc_date_time has the same name in full and abbreviated
198        form.  Also, the month name can by accident match other part of the
199        representation: the day of the week name (for example in Morisyen)
200        or the month number (for example in Japanese).  Thus, cycle months
201        of the year and find all positions that match the month name for
202        each month,  If no common positions are found, the representation
203        does not use the month name.
204        """
205        full_indices = abbr_indices = None
206        for m in range(1, 13):
207            time_tuple = time.struct_time((1999, m, 17, 22, 44, 55, 2, 76, 0))
208            datetime = time.strftime(directive, time_tuple).lower()
209            indices = set(_findall(datetime, self.f_month[m]))
210            if full_indices is None:
211                full_indices = indices
212            else:
213                full_indices &= indices
214            indices = set(_findall(datetime, self.a_month[m]))
215            if abbr_indices is None:
216                abbr_indices = indices
217            else:
218                abbr_indices &= indices
219            if not full_indices and not abbr_indices:
220                return None, None
221        if full_indices:
222            return self.f_month, '%B'
223        if abbr_indices:
224            return self.a_month, '%b'
225        return None, None
226
227    def __find_weekday_format(self, directive):
228        """Find the day of the week format appropriate for the current locale.
229
230        Similar to __find_month_format().
231        """
232        full_indices = abbr_indices = None
233        for wd in range(7):
234            time_tuple = time.struct_time((1999, 3, 17, 22, 44, 55, wd, 76, 0))
235            datetime = time.strftime(directive, time_tuple).lower()
236            indices = set(_findall(datetime, self.f_weekday[wd]))
237            if full_indices is None:
238                full_indices = indices
239            else:
240                full_indices &= indices
241            if self.f_weekday[wd] != self.a_weekday[wd]:
242                indices = set(_findall(datetime, self.a_weekday[wd]))
243            if abbr_indices is None:
244                abbr_indices = indices
245            else:
246                abbr_indices &= indices
247            if not full_indices and not abbr_indices:
248                return None, None
249        if full_indices:
250            return self.f_weekday, '%A'
251        if abbr_indices:
252            return self.a_weekday, '%a'
253        return None, None
254
255    def __calc_timezone(self):
256        # Set self.timezone by using time.tzname.
257        # Do not worry about possibility of time.tzname[0] == time.tzname[1]
258        # and time.daylight; handle that in strptime.
259        try:
260            time.tzset()
261        except AttributeError:
262            pass
263        self.tzname = time.tzname
264        self.daylight = time.daylight
265        no_saving = frozenset({"utc", "gmt", self.tzname[0].lower()})
266        if self.daylight:
267            has_saving = frozenset({self.tzname[1].lower()})
268        else:
269            has_saving = frozenset()
270        self.timezone = (no_saving, has_saving)
271
272
273class TimeRE(dict):
274    """Handle conversion from format directives to regexes."""
275
276    def __init__(self, locale_time=None):
277        """Create keys/values.
278
279        Order of execution is important for dependency reasons.
280
281        """
282        if locale_time:
283            self.locale_time = locale_time
284        else:
285            self.locale_time = LocaleTime()
286        base = super()
287        mapping = {
288            # The " [1-9]" part of the regex is to make %c from ANSI C work
289            'd': r"(?P<d>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
290            'f': r"(?P<f>[0-9]{1,6})",
291            'H': r"(?P<H>2[0-3]|[0-1]\d|\d)",
292            'I': r"(?P<I>1[0-2]|0[1-9]|[1-9]| [1-9])",
293            'G': r"(?P<G>\d\d\d\d)",
294            'j': r"(?P<j>36[0-6]|3[0-5]\d|[1-2]\d\d|0[1-9]\d|00[1-9]|[1-9]\d|0[1-9]|[1-9])",
295            'm': r"(?P<m>1[0-2]|0[1-9]|[1-9])",
296            'M': r"(?P<M>[0-5]\d|\d)",
297            'S': r"(?P<S>6[0-1]|[0-5]\d|\d)",
298            'U': r"(?P<U>5[0-3]|[0-4]\d|\d)",
299            'w': r"(?P<w>[0-6])",
300            'u': r"(?P<u>[1-7])",
301            'V': r"(?P<V>5[0-3]|0[1-9]|[1-4]\d|\d)",
302            # W is set below by using 'U'
303            'y': r"(?P<y>\d\d)",
304            #XXX: Does 'Y' need to worry about having less or more than
305            #     4 digits?
306            'Y': r"(?P<Y>\d\d\d\d)",
307            'z': r"(?P<z>[+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?|(?-i:Z))",
308            'A': self.__seqToRE(self.locale_time.f_weekday, 'A'),
309            'a': self.__seqToRE(self.locale_time.a_weekday, 'a'),
310            'B': self.__seqToRE(self.locale_time.f_month[1:], 'B'),
311            'b': self.__seqToRE(self.locale_time.a_month[1:], 'b'),
312            'p': self.__seqToRE(self.locale_time.am_pm, 'p'),
313            'Z': self.__seqToRE((tz for tz_names in self.locale_time.timezone
314                                        for tz in tz_names),
315                                'Z'),
316            '%': '%'}
317        for d in 'dmyHIMS':
318            mapping['O' + d] = r'(?P<%s>\d\d|\d| \d)' % d
319        mapping['Ow'] = r'(?P<w>\d)'
320        mapping['W'] = mapping['U'].replace('U', 'W')
321        base.__init__(mapping)
322        base.__setitem__('X', self.pattern(self.locale_time.LC_time))
323        base.__setitem__('x', self.pattern(self.locale_time.LC_date))
324        base.__setitem__('c', self.pattern(self.locale_time.LC_date_time))
325
326    def __seqToRE(self, to_convert, directive):
327        """Convert a list to a regex string for matching a directive.
328
329        Want possible matching values to be from longest to shortest.  This
330        prevents the possibility of a match occurring for a value that also
331        a substring of a larger value that should have matched (e.g., 'abc'
332        matching when 'abcdef' should have been the match).
333
334        """
335        to_convert = sorted(to_convert, key=len, reverse=True)
336        for value in to_convert:
337            if value != '':
338                break
339        else:
340            return ''
341        regex = '|'.join(re_escape(stuff) for stuff in to_convert)
342        regex = '(?P<%s>%s' % (directive, regex)
343        return '%s)' % regex
344
345    def pattern(self, format):
346        """Return regex pattern for the format string.
347
348        Need to make sure that any characters that might be interpreted as
349        regex syntax are escaped.
350
351        """
352        # The sub() call escapes all characters that might be misconstrued
353        # as regex syntax.  Cannot use re.escape since we have to deal with
354        # format directives (%m, etc.).
355        format = re_sub(r"([\\.^$*+?\(\){}\[\]|])", r"\\\1", format)
356        format = re_sub(r'\s+', r'\\s+', format)
357        format = re_sub(r"'", "['\u02bc]", format)  # needed for br_FR
358        year_in_format = False
359        day_of_month_in_format = False
360        def repl(m):
361            format_char = m[1]
362            match format_char:
363                case 'Y' | 'y' | 'G':
364                    nonlocal year_in_format
365                    year_in_format = True
366                case 'd':
367                    nonlocal day_of_month_in_format
368                    day_of_month_in_format = True
369            return self[format_char]
370        format = re_sub(r'%(O?.)', repl, format)
371        if day_of_month_in_format and not year_in_format:
372            import warnings
373            warnings.warn("""\
374Parsing dates involving a day of month without a year specified is ambiguious
375and fails to parse leap day. The default behavior will change in Python 3.15
376to either always raise an exception or to use a different default year (TBD).
377To avoid trouble, add a specific year to the input & format.
378See https://github.com/python/cpython/issues/70647.""",
379                          DeprecationWarning,
380                          skip_file_prefixes=(os.path.dirname(__file__),))
381        return format
382
383    def compile(self, format):
384        """Return a compiled re object for the format string."""
385        return re_compile(self.pattern(format), IGNORECASE)
386
387_cache_lock = _thread_allocate_lock()
388# DO NOT modify _TimeRE_cache or _regex_cache without acquiring the cache lock
389# first!
390_TimeRE_cache = TimeRE()
391_CACHE_MAX_SIZE = 5 # Max number of regexes stored in _regex_cache
392_regex_cache = {}
393
394def _calc_julian_from_U_or_W(year, week_of_year, day_of_week, week_starts_Mon):
395    """Calculate the Julian day based on the year, week of the year, and day of
396    the week, with week_start_day representing whether the week of the year
397    assumes the week starts on Sunday or Monday (6 or 0)."""
398    first_weekday = datetime_date(year, 1, 1).weekday()
399    # If we are dealing with the %U directive (week starts on Sunday), it's
400    # easier to just shift the view to Sunday being the first day of the
401    # week.
402    if not week_starts_Mon:
403        first_weekday = (first_weekday + 1) % 7
404        day_of_week = (day_of_week + 1) % 7
405    # Need to watch out for a week 0 (when the first day of the year is not
406    # the same as that specified by %U or %W).
407    week_0_length = (7 - first_weekday) % 7
408    if week_of_year == 0:
409        return 1 + day_of_week - first_weekday
410    else:
411        days_to_week = week_0_length + (7 * (week_of_year - 1))
412        return 1 + days_to_week + day_of_week
413
414
415def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
416    """Return a 2-tuple consisting of a time struct and an int containing
417    the number of microseconds based on the input string and the
418    format string."""
419
420    for index, arg in enumerate([data_string, format]):
421        if not isinstance(arg, str):
422            msg = "strptime() argument {} must be str, not {}"
423            raise TypeError(msg.format(index, type(arg)))
424
425    global _TimeRE_cache, _regex_cache
426    with _cache_lock:
427        locale_time = _TimeRE_cache.locale_time
428        if (_getlang() != locale_time.lang or
429            time.tzname != locale_time.tzname or
430            time.daylight != locale_time.daylight):
431            _TimeRE_cache = TimeRE()
432            _regex_cache.clear()
433            locale_time = _TimeRE_cache.locale_time
434        if len(_regex_cache) > _CACHE_MAX_SIZE:
435            _regex_cache.clear()
436        format_regex = _regex_cache.get(format)
437        if not format_regex:
438            try:
439                format_regex = _TimeRE_cache.compile(format)
440            # KeyError raised when a bad format is found; can be specified as
441            # \\, in which case it was a stray % but with a space after it
442            except KeyError as err:
443                bad_directive = err.args[0]
444                if bad_directive == "\\":
445                    bad_directive = "%"
446                del err
447                raise ValueError("'%s' is a bad directive in format '%s'" %
448                                    (bad_directive, format)) from None
449            # IndexError only occurs when the format string is "%"
450            except IndexError:
451                raise ValueError("stray %% in format '%s'" % format) from None
452            _regex_cache[format] = format_regex
453    found = format_regex.match(data_string)
454    if not found:
455        raise ValueError("time data %r does not match format %r" %
456                         (data_string, format))
457    if len(data_string) != found.end():
458        raise ValueError("unconverted data remains: %s" %
459                          data_string[found.end():])
460
461    iso_year = year = None
462    month = day = 1
463    hour = minute = second = fraction = 0
464    tz = -1
465    gmtoff = None
466    gmtoff_fraction = 0
467    iso_week = week_of_year = None
468    week_of_year_start = None
469    # weekday and julian defaulted to None so as to signal need to calculate
470    # values
471    weekday = julian = None
472    found_dict = found.groupdict()
473    for group_key in found_dict.keys():
474        # Directives not explicitly handled below:
475        #   c, x, X
476        #      handled by making out of other directives
477        #   U, W
478        #      worthless without day of the week
479        if group_key == 'y':
480            year = int(found_dict['y'])
481            # Open Group specification for strptime() states that a %y
482            #value in the range of [00, 68] is in the century 2000, while
483            #[69,99] is in the century 1900
484            if year <= 68:
485                year += 2000
486            else:
487                year += 1900
488        elif group_key == 'Y':
489            year = int(found_dict['Y'])
490        elif group_key == 'G':
491            iso_year = int(found_dict['G'])
492        elif group_key == 'm':
493            month = int(found_dict['m'])
494        elif group_key == 'B':
495            month = locale_time.f_month.index(found_dict['B'].lower())
496        elif group_key == 'b':
497            month = locale_time.a_month.index(found_dict['b'].lower())
498        elif group_key == 'd':
499            day = int(found_dict['d'])
500        elif group_key == 'H':
501            hour = int(found_dict['H'])
502        elif group_key == 'I':
503            hour = int(found_dict['I'])
504            ampm = found_dict.get('p', '').lower()
505            # If there was no AM/PM indicator, we'll treat this like AM
506            if ampm in ('', locale_time.am_pm[0]):
507                # We're in AM so the hour is correct unless we're
508                # looking at 12 midnight.
509                # 12 midnight == 12 AM == hour 0
510                if hour == 12:
511                    hour = 0
512            elif ampm == locale_time.am_pm[1]:
513                # We're in PM so we need to add 12 to the hour unless
514                # we're looking at 12 noon.
515                # 12 noon == 12 PM == hour 12
516                if hour != 12:
517                    hour += 12
518        elif group_key == 'M':
519            minute = int(found_dict['M'])
520        elif group_key == 'S':
521            second = int(found_dict['S'])
522        elif group_key == 'f':
523            s = found_dict['f']
524            # Pad to always return microseconds.
525            s += "0" * (6 - len(s))
526            fraction = int(s)
527        elif group_key == 'A':
528            weekday = locale_time.f_weekday.index(found_dict['A'].lower())
529        elif group_key == 'a':
530            weekday = locale_time.a_weekday.index(found_dict['a'].lower())
531        elif group_key == 'w':
532            weekday = int(found_dict['w'])
533            if weekday == 0:
534                weekday = 6
535            else:
536                weekday -= 1
537        elif group_key == 'u':
538            weekday = int(found_dict['u'])
539            weekday -= 1
540        elif group_key == 'j':
541            julian = int(found_dict['j'])
542        elif group_key in ('U', 'W'):
543            week_of_year = int(found_dict[group_key])
544            if group_key == 'U':
545                # U starts week on Sunday.
546                week_of_year_start = 6
547            else:
548                # W starts week on Monday.
549                week_of_year_start = 0
550        elif group_key == 'V':
551            iso_week = int(found_dict['V'])
552        elif group_key == 'z':
553            z = found_dict['z']
554            if z == 'Z':
555                gmtoff = 0
556            else:
557                if z[3] == ':':
558                    z = z[:3] + z[4:]
559                    if len(z) > 5:
560                        if z[5] != ':':
561                            msg = f"Inconsistent use of : in {found_dict['z']}"
562                            raise ValueError(msg)
563                        z = z[:5] + z[6:]
564                hours = int(z[1:3])
565                minutes = int(z[3:5])
566                seconds = int(z[5:7] or 0)
567                gmtoff = (hours * 60 * 60) + (minutes * 60) + seconds
568                gmtoff_remainder = z[8:]
569                # Pad to always return microseconds.
570                gmtoff_remainder_padding = "0" * (6 - len(gmtoff_remainder))
571                gmtoff_fraction = int(gmtoff_remainder + gmtoff_remainder_padding)
572                if z.startswith("-"):
573                    gmtoff = -gmtoff
574                    gmtoff_fraction = -gmtoff_fraction
575        elif group_key == 'Z':
576            # Since -1 is default value only need to worry about setting tz if
577            # it can be something other than -1.
578            found_zone = found_dict['Z'].lower()
579            for value, tz_values in enumerate(locale_time.timezone):
580                if found_zone in tz_values:
581                    # Deal with bad locale setup where timezone names are the
582                    # same and yet time.daylight is true; too ambiguous to
583                    # be able to tell what timezone has daylight savings
584                    if (time.tzname[0] == time.tzname[1] and
585                       time.daylight and found_zone not in ("utc", "gmt")):
586                        break
587                    else:
588                        tz = value
589                        break
590
591    # Deal with the cases where ambiguities arise
592    # don't assume default values for ISO week/year
593    if iso_year is not None:
594        if julian is not None:
595            raise ValueError("Day of the year directive '%j' is not "
596                             "compatible with ISO year directive '%G'. "
597                             "Use '%Y' instead.")
598        elif iso_week is None or weekday is None:
599            raise ValueError("ISO year directive '%G' must be used with "
600                             "the ISO week directive '%V' and a weekday "
601                             "directive ('%A', '%a', '%w', or '%u').")
602    elif iso_week is not None:
603        if year is None or weekday is None:
604            raise ValueError("ISO week directive '%V' must be used with "
605                             "the ISO year directive '%G' and a weekday "
606                             "directive ('%A', '%a', '%w', or '%u').")
607        else:
608            raise ValueError("ISO week directive '%V' is incompatible with "
609                             "the year directive '%Y'. Use the ISO year '%G' "
610                             "instead.")
611
612    leap_year_fix = False
613    if year is None:
614        if month == 2 and day == 29:
615            year = 1904  # 1904 is first leap year of 20th century
616            leap_year_fix = True
617        else:
618            year = 1900
619
620    # If we know the week of the year and what day of that week, we can figure
621    # out the Julian day of the year.
622    if julian is None and weekday is not None:
623        if week_of_year is not None:
624            week_starts_Mon = True if week_of_year_start == 0 else False
625            julian = _calc_julian_from_U_or_W(year, week_of_year, weekday,
626                                                week_starts_Mon)
627        elif iso_year is not None and iso_week is not None:
628            datetime_result = datetime_date.fromisocalendar(iso_year, iso_week, weekday + 1)
629            year = datetime_result.year
630            month = datetime_result.month
631            day = datetime_result.day
632        if julian is not None and julian <= 0:
633            year -= 1
634            yday = 366 if calendar.isleap(year) else 365
635            julian += yday
636
637    if julian is None:
638        # Cannot pre-calculate datetime_date() since can change in Julian
639        # calculation and thus could have different value for the day of
640        # the week calculation.
641        # Need to add 1 to result since first day of the year is 1, not 0.
642        julian = datetime_date(year, month, day).toordinal() - \
643                  datetime_date(year, 1, 1).toordinal() + 1
644    else:  # Assume that if they bothered to include Julian day (or if it was
645           # calculated above with year/week/weekday) it will be accurate.
646        datetime_result = datetime_date.fromordinal(
647                            (julian - 1) +
648                            datetime_date(year, 1, 1).toordinal())
649        year = datetime_result.year
650        month = datetime_result.month
651        day = datetime_result.day
652    if weekday is None:
653        weekday = datetime_date(year, month, day).weekday()
654    # Add timezone info
655    tzname = found_dict.get("Z")
656
657    if leap_year_fix:
658        # the caller didn't supply a year but asked for Feb 29th. We couldn't
659        # use the default of 1900 for computations. We set it back to ensure
660        # that February 29th is smaller than March 1st.
661        year = 1900
662
663    return (year, month, day,
664            hour, minute, second,
665            weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction
666
667def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"):
668    """Return a time struct based on the input string and the
669    format string."""
670    tt = _strptime(data_string, format)[0]
671    return time.struct_time(tt[:time._STRUCT_TM_ITEMS])
672
673def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"):
674    """Return a class cls instance based on the input string and the
675    format string."""
676    tt, fraction, gmtoff_fraction = _strptime(data_string, format)
677    tzname, gmtoff = tt[-2:]
678    args = tt[:6] + (fraction,)
679    if gmtoff is not None:
680        tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction)
681        if tzname:
682            tz = datetime_timezone(tzdelta, tzname)
683        else:
684            tz = datetime_timezone(tzdelta)
685        args += (tz,)
686
687    return cls(*args)
688