1 # Copyright 2010-2023 Kurt McKee <contactme@kurtmckee.org> 2 # Copyright 2002-2008 Mark Pilgrim 3 # All rights reserved. 4 # 5 # This file is a part of feedparser. 6 # 7 # Redistribution and use in source and binary forms, with or without 8 # modification, are permitted provided that the following conditions are met: 9 # 10 # * Redistributions of source code must retain the above copyright notice, 11 # this list of conditions and the following disclaimer. 12 # * Redistributions in binary form must reproduce the above copyright notice, 13 # this list of conditions and the following disclaimer in the documentation 14 # and/or other materials provided with the distribution. 15 # 16 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' 17 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 # POSSIBILITY OF SUCH DAMAGE. 27 28 import re 29 import time 30 31 # ISO-8601 date parsing routines written by Fazal Majid. 32 # The ISO 8601 standard is very convoluted and irregular - a full ISO 8601 33 # parser is beyond the scope of feedparser and would be a worthwhile addition 34 # to the Python library. 35 # A single regular expression cannot parse ISO 8601 date formats into groups 36 # as the standard is highly irregular (for instance is 030104 2003-01-04 or 37 # 0301-04-01), so we use templates instead. 38 # Please note the order in templates is significant because we need a 39 # greedy match. 40 _iso8601_tmpl = [ 41 'YYYY-?MM-?DD', 42 'YYYY-0MM?-?DD', 43 'YYYY-MM', 44 'YYYY-?OOO', 45 'YY-?MM-?DD', 46 'YY-?OOO', 47 'YYYY', 48 '-YY-?MM', 49 '-OOO', 50 '-YY', 51 '--MM-?DD', 52 '--MM', 53 '---DD', 54 'CC', 55 '', 56 ] 57 58 _iso8601_re = [ 59 tmpl.replace( 60 'YYYY', r'(?P<year>\d{4})').replace( 61 'YY', r'(?P<year>\d\d)').replace( 62 'MM', r'(?P<month>[01]\d)').replace( 63 'DD', r'(?P<day>[0123]\d)').replace( 64 'OOO', r'(?P<ordinal>[0123]\d\d)').replace( 65 'CC', r'(?P<century>\d\d$)') 66 + r'(T?(?P<hour>\d{2}):(?P<minute>\d{2})' 67 + r'(:(?P<second>\d{2}))?' 68 + r'(\.(?P<fracsecond>\d+))?' 69 + r'(?P<tz>[+-](?P<tzhour>\d{2})(:(?P<tzmin>\d{2}))?|Z)?)?' 70 for tmpl in _iso8601_tmpl] 71 try: 72 del tmpl 73 except NameError: 74 pass 75 _iso8601_matches = [re.compile(regex).match for regex in _iso8601_re] 76 try: 77 del regex 78 except NameError: 79 pass 80 81 82 def _parse_date_iso8601(date_string): 83 """Parse a variety of ISO-8601-compatible formats like 20040105""" 84 m = None 85 for _iso8601_match in _iso8601_matches: 86 m = _iso8601_match(date_string) 87 if m: 88 break 89 if not m: 90 return 91 if m.span() == (0, 0): 92 return 93 params = m.groupdict() 94 ordinal = params.get('ordinal', 0) 95 if ordinal: 96 ordinal = int(ordinal) 97 else: 98 ordinal = 0 99 year = params.get('year', '--') 100 if not year or year == '--': 101 year = time.gmtime()[0] 102 elif len(year) == 2: 103 # ISO 8601 assumes current century, i.e. 93 -> 2093, NOT 1993 104 year = 100 * int(time.gmtime()[0] / 100) + int(year) 105 else: 106 year = int(year) 107 month = params.get('month', '-') 108 if not month or month == '-': 109 # ordinals are NOT normalized by mktime, we simulate them 110 # by setting month=1, day=ordinal 111 if ordinal: 112 month = 1 113 else: 114 month = time.gmtime()[1] 115 month = int(month) 116 day = params.get('day', 0) 117 if not day: 118 # see above 119 if ordinal: 120 day = ordinal 121 elif params.get('century', 0) or \ 122 params.get('year', 0) or params.get('month', 0): 123 day = 1 124 else: 125 day = time.gmtime()[2] 126 else: 127 day = int(day) 128 # special case of the century - is the first year of the 21st century 129 # 2000 or 2001 ? The debate goes on... 130 if 'century' in params: 131 year = (int(params['century']) - 1) * 100 + 1 132 # in ISO 8601 most fields are optional 133 for field in ['hour', 'minute', 'second', 'tzhour', 'tzmin']: 134 if not params.get(field, None): 135 params[field] = 0 136 hour = int(params.get('hour', 0)) 137 minute = int(params.get('minute', 0)) 138 second = int(float(params.get('second', 0))) 139 # weekday is normalized by mktime(), we can ignore it 140 weekday = 0 141 daylight_savings_flag = -1 142 tm = [year, month, day, hour, minute, second, weekday, 143 ordinal, daylight_savings_flag] 144 # ISO 8601 time zone adjustments 145 tz = params.get('tz') 146 if tz and tz != 'Z': 147 if tz[0] == '-': 148 tm[3] += int(params.get('tzhour', 0)) 149 tm[4] += int(params.get('tzmin', 0)) 150 elif tz[0] == '+': 151 tm[3] -= int(params.get('tzhour', 0)) 152 tm[4] -= int(params.get('tzmin', 0)) 153 else: 154 return None 155 # Python's time.mktime() is a wrapper around the ANSI C mktime(3c) 156 # which is guaranteed to normalize d/m/y/h/m/s. 157 # Many implementations have bugs, but we'll pretend they don't. 158 return time.localtime(time.mktime(tuple(tm))) 159