def parse_time(timestring): ''' Parses ISO 8601 times into datetime.time objects. Following ISO 8601 formats are supported: (as decimal separator a ',' or a '.' is allowed) hhmmss.ssTZD basic complete time hh:mm:ss.ssTZD extended compelte time hhmm.mmTZD basic reduced accuracy time hh:mm.mmTZD extended reduced accuracy time hh.hhTZD basic reduced accuracy time TZD is the time zone designator which can be in the following format: no designator indicates local time zone Z UTC +-hhmm basic hours and minutes +-hh:mm extended hours and minutes +-hh hours ''' isotimes = build_time_regexps() for pattern in isotimes: match = pattern.match(timestring) if match: groups = match.groupdict() for key, value in groups.items(): if value is not None: groups[key] = value.replace(',', '.') tzinfo = build_tzinfo(groups['tzname'], groups['tzsign'], int(groups['tzhour'] or 0), int(groups['tzmin'] or 0)) if 'second' in groups: # round to microseconds if fractional seconds are more precise second = Decimal(groups['second']).quantize(Decimal('.000001')) second = min(second, Decimal('59.999999')) microsecond = (second - int(second)) * int(1e6) # int(...) ... no rounding # to_integral() ... rounding return time(int(groups['hour']), int(groups['minute']), int(second), int(microsecond.to_integral()), tzinfo) if 'minute' in groups: minute = Decimal(groups['minute']) second = (minute - int(minute)) * 60 microsecond = (second - int(second)) * int(1e6) return time(int(groups['hour']), int(minute), int(second), int(microsecond.to_integral()), tzinfo) else: microsecond, second, minute = 0, 0, 0 hour = Decimal(groups['hour']) minute = (hour - int(hour)) * 60 second = (minute - int(minute)) * 60 microsecond = (second - int(second)) * int(1e6) return time(int(hour), int(minute), int(second), int(microsecond.to_integral()), tzinfo) raise ISO8601Error('Unrecognised ISO 8601 time format: %r' % timestring)
def parse_time(timestring): ''' Parses ISO 8601 times into datetime.time objects. Following ISO 8601 formats are supported: (as decimal separator a ',' or a '.' is allowed) hhmmss.ssTZD basic complete time hh:mm:ss.ssTZD extended compelte time hhmm.mmTZD basic reduced accuracy time hh:mm.mmTZD extended reduced accuracy time hh.hhTZD basic reduced accuracy time TZD is the time zone designator which can be in the following format: no designator indicates local time zone Z UTC +-hhmm basic hours and minutes +-hh:mm extended hours and minutes +-hh hours ''' isotimes = build_time_regexps() for pattern in isotimes: match = pattern.match(timestring) if match: groups = match.groupdict() for key, value in groups.items(): if value is not None: groups[key] = value.replace(',', '.') tzinfo = build_tzinfo(groups['tzname'], groups['tzsign'], int(groups['tzhour'] or 0), int(groups['tzmin'] or 0)) if 'second' in groups: frac, second = math.modf(float(groups['second'])) microsecond = frac * 1e6 return time(int(groups['hour']), int(groups['minute']), int(second), int(microsecond), tzinfo) if 'minute' in groups: frac, minute = math.modf(float(groups['minute'])) frac, second = math.modf(frac * 60.0) microsecond = frac * 1e6 return time(int(groups['hour']), int(minute), int(second), int(microsecond), tzinfo) else: microsecond, second, minute = 0, 0, 0 frac, hour = math.modf(float(groups['hour'])) frac, minute = math.modf(frac * 60.0) frac, second = math.modf(frac * 60.0) microsecond = frac * 1e6 return time(int(hour), int(minute), int(second), int(microsecond), tzinfo) raise ISO8601Error('Unrecognised ISO 8601 time format: %r' % timestring)
def parse_datetime(datetimestring): """ Parses ISO 8601 date-times into datetime.datetime objects. This function uses parse_date and parse_time to do the job, so it allows more combinations of date and time representations, than the actual ISO 8601:2004 standard allows. """ try: datestring, timestring = datetimestring.split("T") except ValueError: raise ISO8601Error("ISO 8601 time designator 'T' missing. Unable to" " parse datetime string %r" % datetimestring) tmpdate = parse_date(datestring) tmptime = parse_time(timestring) return datetime.combine(tmpdate, tmptime)
def __str__(self): ret = "" if self.repeat: ret += "R%d/" % self.repeat if self.start and self.end: ret += "%s/%s" % (datetime_isoformat( self.start), datetime_isoformat(self.end)) elif self.start and self.duration: ret += "%s/%s" % (datetime_isoformat( self.start), duration_isoformat(self.duration)) elif self.duration and self.end: ret += "%s/%s" % (duration_isoformat( self.duration), datetime_isoformat(self.end)) else: raise ISO8601Error( "Could not produce a valid ISO8601 interval string") return ret
def parse_tzinfo(tzstring): ''' Parses ISO 8601 time zone designators to tzinfo objecs. A time zone designator can be in the following format: no designator indicates local time zone Z UTC +-hhmm basic hours and minutes +-hh:mm extended hours and minutes +-hh hours ''' match = TZ_RE.match(tzstring) if match: groups = match.groupdict() return build_tzinfo(groups['tzname'], groups['tzsign'], int(groups['tzhour'] or 0), int(groups['tzmin'] or 0)) raise ISO8601Error('%s not a valid time zone info' % tzstring)
def parse_tzinfo(tzstring): """ Parses ISO 8601 time zone designators to tzinfo objects. A time zone designator can be in the following format: no designator indicates local time zone Z UTC +-hhmm basic hours and minutes +-hh:mm extended hours and minutes +-hh hours """ match = TZ_RE.match(tzstring) if match: groups = match.groupdict() return build_tzinfo( groups["tzname"], groups["tzsign"], int(groups["tzhour"] or 0), int(groups["tzmin"] or 0), ) raise ISO8601Error("%s not a valid time zone info" % tzstring)
def interval_isoformat(arg1, arg2, repeat=None, fmt=None): output_r = output_1 = output_2 = None # check format fmt_segments = [None, None, None] if fmt: fmt_segments = fmt.split("/") if len(fmt_segments) == 3: r_fmt = fmt_segments[0] # only allow R or Rn if r_fmt == "R": output_r = "R" if r_fmt == "Rn": output_r = "R" if repeat: if not isinstance(repeat, (int, long)) or repeat < 0: raise TypeError( "repeat must be an whole, non-negative number") output_r += repeat # remove repeat format and proceed with other 2 arg formatting fmt_segments = fmt_segments[1:] else: if repeat: output_r = "R" if isinstance(arg1, datetime): str_1 = datetime_isoformat(arg1, fmt_segments[0]) #assert (isinstance(arg2, datetime, time, date, timedelta)) if isinstance(arg2, datetime): str_2 = datetime_isoformat(arg2, format=fmt_segments[1]) elif isinstance(arg2, date): str_2 = date_isoformat(arg2, format=fmt_segments[1]) elif isinstance(arg1, datetime.timedelta): str1 = duration_isoformat(arg1) #assert (isinstance(arg2, datetime)) else: raise ISO8601Error( "arg1 must be a datetime, time, date, or timedelta object")
def parse_date(datestring, yeardigits=4, expanded=False, defaultmonth=1, defaultday=1): """ Parse an ISO 8601 date string into a datetime.date object. As the datetime.date implementation is limited to dates starting from 0001-01-01, negative dates (BC) and year 0 can not be parsed by this method. For incomplete dates, this method chooses the first day for it. For instance if only a century is given, this method returns the 1st of January in year 1 of this century. supported formats: (expanded formats are shown with 6 digits for year) YYYYMMDD +-YYYYYYMMDD basic complete date YYYY-MM-DD +-YYYYYY-MM-DD extended complete date YYYYWwwD +-YYYYYYWwwD basic complete week date YYYY-Www-D +-YYYYYY-Www-D extended complete week date YYYYDDD +-YYYYYYDDD basic ordinal date YYYY-DDD +-YYYYYY-DDD extended ordinal date YYYYWww +-YYYYYYWww basic incomplete week date YYYY-Www +-YYYYYY-Www extended incomplete week date YYYMM +-YYYYYYMM basic incomplete month date YYY-MM +-YYYYYY-MM incomplete month date YYYY +-YYYYYY incomplete year date YY +-YYYY incomplete century date @param datestring: the ISO date string to parse @param yeardigits: how many digits are used to represent a year @param expanded: if True then +/- signs are allowed. This parameter is forced to True, if yeardigits != 4 @return: a datetime.date instance represented by datestring @raise ISO8601Error: if this function can not parse the datestring @raise ValueError: if datestring can not be represented by datetime.date """ if yeardigits != 4: expanded = True isodates = build_date_regexps(yeardigits, expanded) for pattern in isodates: match = pattern.match(datestring) if match: groups = match.groupdict() # sign, century, year, month, week, day, # FIXME: negative dates not possible with python standard types sign = (groups["sign"] == "-" and -1) or 1 if "century" in groups: return date( sign * (int(groups["century"]) * 100 + 1), defaultmonth, defaultday ) if "month" not in groups: # weekdate or ordinal date ret = date(sign * int(groups["year"]), 1, 1) if "week" in groups: isotuple = ret.isocalendar() if "day" in groups: days = int(groups["day"] or 1) else: days = 1 # if first week in year, do weeks-1 return ret + timedelta( weeks=int(groups["week"]) - (((isotuple[1] == 1) and 1) or 0), days=-isotuple[2] + days, ) elif "day" in groups: # ordinal date return ret + timedelta(days=int(groups["day"]) - 1) else: # year date return ret.replace(month=defaultmonth, day=defaultday) # year-, month-, or complete date if "day" not in groups or groups["day"] is None: day = defaultday else: day = int(groups["day"]) return date( sign * int(groups["year"]), int(groups["month"]) or defaultmonth, day ) raise ISO8601Error("Unrecognised ISO 8601 date format: %r" % datestring)
def parse_duration(datestring): """ Parses an ISO 8601 durations into datetime.timedelta or Duration objects. If the ISO date string does not contain years or months, a timedelta instance is returned, else a Duration instance is returned. The following duration formats are supported: -PnnW duration in weeks -PnnYnnMnnDTnnHnnMnnS complete duration specification -PYYYYMMDDThhmmss basic alternative complete date format -PYYYY-MM-DDThh:mm:ss extended alternative complete date format -PYYYYDDDThhmmss basic alternative ordinal date format -PYYYY-DDDThh:mm:ss extended alternative ordinal date format The '-' is optional. Limitations: ISO standard defines some restrictions about where to use fractional numbers and which component and format combinations are allowed. This parser implementation ignores all those restrictions and returns something when it is able to find all necessary components. In detail: it does not check, whether only the last component has fractions. it allows weeks specified with all other combinations The alternative format does not support durations with years, months or days set to 0. """ if not isinstance(datestring, basestring): raise TypeError("Expecting a string %r" % datestring) match = ISO8601_PERIOD_REGEX.match(datestring) if not match: # try alternative format: if datestring.startswith("P"): durdt = parse_datetime(datestring[1:]) if durdt.year != 0 or durdt.month != 0: # create Duration ret = Duration(days=durdt.day, seconds=durdt.second, microseconds=durdt.microsecond, minutes=durdt.minute, hours=durdt.hour, months=durdt.month, years=durdt.year) else: # FIXME: currently not possible in alternative format # create timedelta ret = timedelta(days=durdt.day, seconds=durdt.second, microseconds=durdt.microsecond, minutes=durdt.minute, hours=durdt.hour) return ret raise ISO8601Error("Unable to parse duration string %r" % datestring) groups = match.groupdict() for key, val in groups.items(): if key not in ('separator', 'sign'): if val is None: groups[key] = "0n" #print groups[key] if key in ('years', 'months'): groups[key] = Decimal(groups[key][:-1].replace(',', '.')) else: # these values are passed into a timedelta object, which works with floats. groups[key] = float(groups[key][:-1].replace(',', '.')) if groups["years"] == 0 and groups["months"] == 0: ret = timedelta(days=groups["days"], hours=groups["hours"], minutes=groups["minutes"], seconds=groups["seconds"], weeks=groups["weeks"]) if groups["sign"] == '-': ret = timedelta(0) - ret else: ret = Duration(years=groups["years"], months=groups["months"], days=groups["days"], hours=groups["hours"], minutes=groups["minutes"], seconds=groups["seconds"], weeks=groups["weeks"]) if groups["sign"] == '-': ret = Duration(0) - ret return ret
def parse_interval(interval_string): if not isinstance(interval_string, string_types): raise TypeError("Expecing a string") segment_count = interval_string.count(SEGMENT_DELIM) if segment_count < 1 or segment_count > 2: raise ISO8601Error( "Improper number of interval string segments. Must have 1 or 2") segments = interval_string.split(SEGMENT_DELIM) for idx, seg in enumerate(segments): if len(seg) == 0: return ISO8601Error("Interval segment index %s was empty" % idx) count = None if len(segments) == 3: # Rn/start/end # Rn/start/duration # Rn/duration/end s0 = segments[0] match = ISO8601_REPEAT_REGEX.match(s0) if not match: raise ISO8601Error("Repeat notation did not match expected") groups = match.groupdict() count = groups.get("count", None) if len(count) > 0: count = int(count) segments = segments[1:] s0 = segments[0] s1 = segments[1] # remaining segments are either # 1) start/end. # start must be a fully specified datetime format # end can either be a time, date, or datetime # 2) start/duration # start must be a fully specified datetime format # duration must be a valid duration format # 3) duration/end # duration must be a valid duration format # end must be a fully specified datetime format start = None end = None duration = None try: # (1) start = parse_datetime(s0) print("second to last term is a datetime") except: try: duration = parse_duration(s0) print("second to last term is a datetime") except: raise ISO8601Error( "First term after repeat must be either " + "a fully specified datetime or a valid duration") # look at last term # this isn't the prettiest way to do it, but it is effective # could also build the regexes from other modules, but delegation avoids code duplication if start: # last term must be a duration, date, time or datetime try: end = parse_datetime(s1) print("last term is a datetime") except: try: end = parse_date(s1) print("last term is a date") except: try: end = parse_time(s1) print("last term is a time") except: try: duration = parse_duration(s1) print("last term is a duration") except: raise ISO8601Error( "When first term after repeat is a datetime, " + "last term must be either a duration, datetime, date, or time" ) elif duration: # last term must be the end datetime try: end = parse_datetime(s1) except: raise ISO8601Error("If first term after repeat is a duration, " + "last term must be a datetime") interval = Interval(start=start, end=end, duration=duration, repeat=count) print(interval)
def parse_time(timestring): """ Parses ISO 8601 times into datetime.time objects. Following ISO 8601 formats are supported: (as decimal separator a ',' or a '.' is allowed) hhmmss.ssTZD basic complete time hh:mm:ss.ssTZD extended complete time hhmm.mmTZD basic reduced accuracy time hh:mm.mmTZD extended reduced accuracy time hh.hhTZD basic reduced accuracy time TZD is the time zone designator which can be in the following format: no designator indicates local time zone Z UTC +-hhmm basic hours and minutes +-hh:mm extended hours and minutes +-hh hours """ isotimes = build_time_regexps() for pattern in isotimes: match = pattern.match(timestring) if match: groups = match.groupdict() for key, value in groups.items(): if value is not None: groups[key] = value.replace(",", ".") tzinfo = build_tzinfo( groups["tzname"], groups["tzsign"], int(groups["tzhour"] or 0), int(groups["tzmin"] or 0), ) if "second" in groups: second = Decimal(groups["second"]).quantize( Decimal(".000001"), rounding=ROUND_FLOOR ) microsecond = (second - int(second)) * int(1e6) # int(...) ... no rounding # to_integral() ... rounding return time( int(groups["hour"]), int(groups["minute"]), int(second), int(microsecond.to_integral()), tzinfo, ) if "minute" in groups: minute = Decimal(groups["minute"]) second = Decimal((minute - int(minute)) * 60).quantize( Decimal(".000001"), rounding=ROUND_FLOOR ) microsecond = (second - int(second)) * int(1e6) return time( int(groups["hour"]), int(minute), int(second), int(microsecond.to_integral()), tzinfo, ) else: microsecond, second, minute = 0, 0, 0 hour = Decimal(groups["hour"]) minute = (hour - int(hour)) * 60 second = (minute - int(minute)) * 60 microsecond = (second - int(second)) * int(1e6) return time( int(hour), int(minute), int(second), int(microsecond.to_integral()), tzinfo, ) raise ISO8601Error("Unrecognised ISO 8601 time format: %r" % timestring)