diff --git a/src/calibre/utils/iso8601.py b/src/calibre/utils/iso8601.py index a425056f45..93f9cedb98 100644 --- a/src/calibre/utils/iso8601.py +++ b/src/calibre/utils/iso8601.py @@ -5,8 +5,6 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) from datetime import datetime -from decimal import Decimal -import re from dateutil.tz import tzlocal, tzutc, tzoffset @@ -31,132 +29,22 @@ local_tz = SafeLocalTimeZone() del tzutc, tzlocal UNDEFINED_DATE = datetime(101,1,1, tzinfo=utc_tz) -if hasattr(speedup, 'parse_iso8601'): # For people running from source without updated binaries - def parse_iso8601(date_string, assume_utc=False, as_utc=True): - if not date_string: - return UNDEFINED_DATE - dt, aware, tzseconds = speedup.parse_iso8601(date_string) - tz = utc_tz if assume_utc else local_tz - if aware: # timezone was specified - if tzseconds == 0: - tz = utc_tz - else: - sign = '-' if tzseconds < 0 else '+' - description = "%s%02d:%02d" % (sign, abs(tzseconds) // 3600, (abs(tzseconds) % 3600) // 60) - tz = tzoffset(description, tzseconds) - dt = dt.replace(tzinfo=tz) - if as_utc and tz is utc_tz: - return dt - return dt.astimezone(utc_tz if as_utc else local_tz) -else: - # Pure python implementation - _iso_pat = None - - def iso_pat(): - global _iso_pat - if _iso_pat is None: - _iso_pat = re.compile( - # Adapted from http://delete.me.uk/2005/03/iso8601.html - r""" - (?P[0-9]{4}) - ( - ( - (-(?P[0-9]{1,2})) - | - (?P[0-9]{2}) - (?!$) # Don't allow YYYYMM - ) - ( - ( - (-(?P[0-9]{1,2})) - | - (?P[0-9]{2}) - ) - ( - ( - (?P[ T]) - (?P[0-9]{2}) - (:{0,1}(?P[0-9]{2})){0,1} - ( - :{0,1}(?P[0-9]{1,2}) - ([.,](?P[0-9]+)){0,1} - ){0,1} - (?P - Z - | - ( - (?P[-+]) - (?P[0-9]{2}) - :{0,1} - (?P[0-9]{2}){0,1} - ) - ){0,1} - ){0,1} - ) - ){0,1} # YYYY-MM - ){0,1} # YYYY only - $ - """, re.VERBOSE) - return _iso_pat - - def to_int(d, key, default_to_zero=False, default=None, required=True): - """Pull a value from the dict and convert to int - - :param default_to_zero: If the value is None or empty, treat it as zero - :param default: If the value is missing in the dict use this default - - """ - value = d.get(key) or default - if (value is None or value == '') and default_to_zero: - return 0 - if value is None: - if required: - raise ValueError("Unable to read %s from %s" % (key, d)) +def parse_iso8601(date_string, assume_utc=False, as_utc=True): + if not date_string: + return UNDEFINED_DATE + dt, aware, tzseconds = speedup.parse_iso8601(date_string) + tz = utc_tz if assume_utc else local_tz + if aware: # timezone was specified + if tzseconds == 0: + tz = utc_tz else: - return int(value) - - def parse_timezone(matches, default_timezone=utc_tz): - """Parses ISO 8601 time zone specs into tzinfo offsets - - """ - - if matches["timezone"] == "Z": - return utc_tz - # This isn't strictly correct, but it's common to encounter dates without - # timezones so I'll assume the default (which defaults to UTC). - # Addresses issue 4. - if matches["timezone"] is None: - return default_timezone - sign = matches["tz_sign"] - hours = to_int(matches, "tz_hour") - minutes = to_int(matches, "tz_minute", default_to_zero=True) - description = "%s%02d:%02d" % (sign, hours, minutes) - if sign == "-": - hours = -hours - minutes = -minutes - return tzoffset(description, 3600*hours + 60*minutes) - - def parse_iso8601(date_string, assume_utc=False, as_utc=True): - if not date_string: - return UNDEFINED_DATE - if isinstance(date_string, bytes): - date_string = date_string.decode('ascii') - m = iso_pat().match(date_string) - if m is None: - raise ValueError('%r is not a valid ISO8601 date' % date_string) - groups = m.groupdict() - tz = parse_timezone(groups, default_timezone=utc_tz if assume_utc else local_tz) - groups["second_fraction"] = int(Decimal("0.%s" % (groups["second_fraction"] or 0)) * Decimal("1000000.0")) - return datetime( - year=to_int(groups, "year"), - month=to_int(groups, "month", default=to_int(groups, "monthdash", required=False, default=1)), - day=to_int(groups, "day", default=to_int(groups, "daydash", required=False, default=1)), - hour=to_int(groups, "hour", default_to_zero=True), - minute=to_int(groups, "minute", default_to_zero=True), - second=to_int(groups, "second", default_to_zero=True), - microsecond=groups["second_fraction"], - tzinfo=tz, - ).astimezone(utc_tz if as_utc else local_tz) + sign = '-' if tzseconds < 0 else '+' + description = "%s%02d:%02d" % (sign, abs(tzseconds) // 3600, (abs(tzseconds) % 3600) // 60) + tz = tzoffset(description, tzseconds) + dt = dt.replace(tzinfo=tz) + if as_utc and tz is utc_tz: + return dt + return dt.astimezone(utc_tz if as_utc else local_tz) if __name__ == '__main__': import sys