def parse_duration(isodurationstr, builder=PythonTimeBuilder): #Given a string representing an ISO 8601 duration, return a #a duration built by the given builder. Valid formats are: # #PnYnMnDTnHnMnS (or any reduced precision equivalent) #PnW #P<date>T<time> if compat.is_string(isodurationstr) is False: raise ValueError('Duration must be string.') if len(isodurationstr) == 0: raise ISOFormatError( '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr)) if isodurationstr[0] != 'P': raise ISOFormatError('ISO 8601 duration must start with a P.') #If Y, M, D, H, S, or W are in the string, #assume it is a specified duration if _has_any_component(isodurationstr, ['Y', 'M', 'D', 'H', 'S', 'W']) is True: parseresult = _parse_duration_prescribed(isodurationstr) return builder.build_duration(**parseresult) if isodurationstr.find('T') != -1: parseresult = _parse_duration_combined(isodurationstr) return builder.build_duration(**parseresult) raise ISOFormatError( '"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr))
def test_is_string(self): self.assertTrue(is_string('asdf')) self.assertTrue(is_string('')) if PY2 is True: self.assertTrue(is_string(unicode('asdf'))) self.assertFalse(is_string(None)) self.assertFalse(is_string(123)) self.assertFalse(is_string(4.56)) self.assertFalse(is_string([])) self.assertFalse(is_string({}))
def get_time_resolution(isotimestr): #Valid time formats are: # #hh:mm:ss #hhmmss #hh:mm #hhmm #hh #hh:mm:ssZ #hhmmssZ #hh:mmZ #hhmmZ #hhZ #hh:mm:ss±hh:mm #hhmmss±hh:mm #hh:mm±hh:mm #hhmm±hh:mm #hh±hh:mm #hh:mm:ss±hhmm #hhmmss±hhmm #hh:mm±hhmm #hhmm±hhmm #hh±hhmm #hh:mm:ss±hh #hhmmss±hh #hh:mm±hh #hhmm±hh #hh±hh if is_string(isotimestr) is False: raise ValueError('Time must be string.') timestr = _split_tz(isotimestr)[0].replace(':', '') if (len(timestr) == 0 or timestr[0].isdigit() is False or timestr[-1].isdigit() is False): raise ISOFormatError( '"{0}" is not a valid ISO 8601 time.'.format(timestr)) #Format must be hhmmss, hhmm, or hh timestrlen = find_separator(timestr) if timestrlen == -1: timestrlen = len(timestr) if timestrlen == 6: #hhmmss return TimeResolution.Seconds elif timestrlen == 4: #hhmm return TimeResolution.Minutes elif timestrlen == 2: #hh return TimeResolution.Hours raise ISOFormatError( '"{0}" is not a valid ISO 8601 time.'.format(isotimestr))
def parse_timezone(tzstr, builder=PythonTimeBuilder): # tzstr can be Z, ±hh:mm, ±hhmm, ±hh if is_string(tzstr) is False: raise ValueError("Time zone must be string.") if len(tzstr) == 1 and tzstr[0] == "Z": return builder.build_timezone(negative=False, Z=True, name=tzstr) elif len(tzstr) == 6: # ±hh:mm hourstr = tzstr[1:3] minutestr = tzstr[4:6] if tzstr[0] == "-" and hourstr == "00" and minutestr == "00": raise ISOFormatError("Negative ISO 8601 time offset must not " "be 0.") elif len(tzstr) == 5: # ±hhmm hourstr = tzstr[1:3] minutestr = tzstr[3:5] if tzstr[0] == "-" and hourstr == "00" and minutestr == "00": raise ISOFormatError("Negative ISO 8601 time offset must not " "be 0.") elif len(tzstr) == 3: # ±hh hourstr = tzstr[1:3] minutestr = None if tzstr[0] == "-" and hourstr == "00": raise ISOFormatError("Negative ISO 8601 time offset must not " "be 0.") else: raise ISOFormatError( '"{0}" is not a valid ISO 8601 time offset.'.format(tzstr)) for componentstr in [hourstr, minutestr]: if componentstr is not None: if componentstr.isdigit() is False: raise ISOFormatError( '"{0}" is not a valid ISO 8601 time offset.'.format(tzstr)) if tzstr[0] == "+": return builder.build_timezone(negative=False, hh=hourstr, mm=minutestr, name=tzstr) if tzstr[0] == "-": return builder.build_timezone(negative=True, hh=hourstr, mm=minutestr, name=tzstr) raise ISOFormatError( '"{0}" is not a valid ISO 8601 time offset.'.format(tzstr))
def test_is_string(self): self.assertTrue(is_string("asdf")) self.assertTrue(is_string("")) # pylint: disable=undefined-variable if PY2 is True: self.assertTrue(is_string(unicode("asdf"))) self.assertFalse(is_string(None)) self.assertFalse(is_string(123)) self.assertFalse(is_string(4.56)) self.assertFalse(is_string([])) self.assertFalse(is_string({}))
def parse_timezone(tzstr, builder=PythonTimeBuilder): #tzstr can be Z, ±hh:mm, ±hhmm, ±hh if is_string(tzstr) is False: raise ValueError('Time zone must be string.') if 'Z' in tzstr: if len(tzstr) != 1: raise ISOFormatError( '"{0}" is not a valid ISO 8601 time offset.'.format(tzstr)) return builder.build_timezone(negative=False, Z=True, name=tzstr) elif len(tzstr) == 6: #±hh:mm hourstr = tzstr[1:3] minutestr = tzstr[4:6] if tzstr[0] == '-' and hourstr == '00' and minutestr == '00': raise ISOFormatError('Negative ISO 8601 time offset must not ' 'be 0.') elif len(tzstr) == 5: #±hhmm hourstr = tzstr[1:3] minutestr = tzstr[3:5] if tzstr[0] == '-' and hourstr == '00' and minutestr == '00': raise ISOFormatError('Negative ISO 8601 time offset must not ' 'be 0.') elif len(tzstr) == 3: #±hh hourstr = tzstr[1:3] minutestr = None if tzstr[0] == '-' and hourstr == '00': raise ISOFormatError('Negative ISO 8601 time offset must not ' 'be 0.') else: raise ISOFormatError( '"{0}" is not a valid ISO 8601 time offset.'.format(tzstr)) if tzstr[0] == '+': return builder.build_timezone(negative=False, hh=hourstr, mm=minutestr, name=tzstr) elif tzstr[0] == '-': return builder.build_timezone(negative=True, hh=hourstr, mm=minutestr, name=tzstr) raise ISOFormatError( '"{0}" is not a valid ISO 8601 time offset.'.format(tzstr))
def parse_interval( isointervalstr, intervaldelimiter="/", datetimedelimiter="T", builder=PythonTimeBuilder, ): # Given a string representing an ISO 8601 interval, return an # interval built by the given builder. Valid formats are: # # <start>/<end> # <start>/<duration> # <duration>/<end> # # The <start> and <end> values can represent dates, or datetimes, # not times. # # The format: # # <duration> # # Is expressly not supported as there is no way to provide the additional # required context. if is_string(isointervalstr) is False: raise ValueError("Interval must be string.") if len(isointervalstr) == 0: raise ISOFormatError("Interval string is empty.") if isointervalstr[0] == "R": raise ISOFormatError("ISO 8601 repeating intervals must be parsed " "with parse_repeating_interval.") intervaldelimitercount = isointervalstr.count(intervaldelimiter) if intervaldelimitercount == 0: raise ISOFormatError('Interval delimiter "{0}" is not in interval ' 'string "{1}".'.format(intervaldelimiter, isointervalstr)) if intervaldelimitercount > 1: raise ISOFormatError( "{0} is not a valid ISO 8601 interval".format(isointervalstr)) return _parse_interval(isointervalstr, builder, intervaldelimiter, datetimedelimiter)
def parse_datetime(isodatetimestr, delimiter="T", builder=PythonTimeBuilder): # Given a string in ISO 8601 date time format, return a datetime.datetime # object that corresponds to the given date time. # By default, the ISO 8601 specified T delimiter is used to split the # date and time (<date>T<time>). Fixed offset tzdata will be included # if UTC offset is given in the input string. if is_string(isodatetimestr) is False: raise ValueError("Date time must be string.") if delimiter not in isodatetimestr: raise ISOFormatError('Delimiter "{0}" is not in combined date time ' 'string "{1}".'.format(delimiter, isodatetimestr)) isodatestr, isotimestr = isodatetimestr.split(delimiter, 1) datepart = parse_date(isodatestr, builder=TupleBuilder) timepart = parse_time(isotimestr, builder=TupleBuilder) return builder.build_datetime(datepart, timepart)
def parse_repeating_interval(isointervalstr, intervaldelimiter='/', datetimedelimiter='T', builder=PythonTimeBuilder): #Given a string representing an ISO 8601 interval repeating, return an #interval built by the given builder. Valid formats are: # #Rnn/<interval> #R/<interval> if is_string(isointervalstr) is False: raise ValueError('Interval must be string.') if len(isointervalstr) == 0: raise ISOFormatError('Repeating interval string is empty.') if intervaldelimiter not in isointervalstr: raise ISOFormatError('Interval delimiter "{0}" is not in interval ' 'string "{1}".' .format(intervaldelimiter, isointervalstr)) if isointervalstr[0] != 'R': raise ISOFormatError('ISO 8601 repeating interval must start ' 'with an R.') #Parse the number of iterations iterationpart, intervalpart = isointervalstr.split(intervaldelimiter, 1) if len(iterationpart) > 1: R = False Rnn = iterationpart[1:] else: R = True Rnn = None interval = _parse_interval(intervalpart, TupleBuilder, intervaldelimiter, datetimedelimiter) return builder.build_repeating_interval(R=R, Rnn=Rnn, interval=interval)
def parse_time(isotimestr, builder=PythonTimeBuilder): # Given a string in any ISO 8601 time format, return a datetime.time object # that corresponds to the given time. Fixed offset tzdata will be included # if UTC offset is given in the input string. Valid time formats are: # # hh:mm:ss # hhmmss # hh:mm # hhmm # hh # hh:mm:ssZ # hhmmssZ # hh:mmZ # hhmmZ # hhZ # hh:mm:ss±hh:mm # hhmmss±hh:mm # hh:mm±hh:mm # hhmm±hh:mm # hh±hh:mm # hh:mm:ss±hhmm # hhmmss±hhmm # hh:mm±hhmm # hhmm±hhmm # hh±hhmm # hh:mm:ss±hh # hhmmss±hh # hh:mm±hh # hhmm±hh # hh±hh if is_string(isotimestr) is False: raise ValueError("Time must be string.") if len(isotimestr) == 0: raise ISOFormatError( '"{0}" is not a valid ISO 8601 time.'.format(isotimestr)) timestr = normalize(isotimestr) hourstr = None minutestr = None secondstr = None tzstr = None fractionalstr = None # Split out the timezone for delimiter in TIMEZONE_DELIMITERS: delimiteridx = timestr.find(delimiter) if delimiteridx != -1: tzstr = timestr[delimiteridx:] timestr = timestr[0:delimiteridx] # Split out the fractional component if timestr.find(".") != -1: timestr, fractionalstr = timestr.split(".", 1) if fractionalstr.isdigit() is False: raise ISOFormatError( '"{0}" is not a valid ISO 8601 time.'.format(isotimestr)) if len(timestr) == 2: # hh hourstr = timestr elif len(timestr) == 4 or len(timestr) == 5: # hh:mm # hhmm if timestr.count(":") == 1: hourstr, minutestr = timestr.split(":") else: hourstr = timestr[0:2] minutestr = timestr[2:] elif len(timestr) == 6 or len(timestr) == 8: # hh:mm:ss # hhmmss if timestr.count(":") == 2: hourstr, minutestr, secondstr = timestr.split(":") else: hourstr = timestr[0:2] minutestr = timestr[2:4] secondstr = timestr[4:] else: raise ISOFormatError( '"{0}" is not a valid ISO 8601 time.'.format(isotimestr)) for componentstr in [hourstr, minutestr, secondstr]: if componentstr is not None and componentstr.isdigit() is False: raise ISOFormatError( '"{0}" is not a valid ISO 8601 time.'.format(isotimestr)) if fractionalstr is not None: if secondstr is not None: secondstr = secondstr + "." + fractionalstr elif minutestr is not None: minutestr = minutestr + "." + fractionalstr else: hourstr = hourstr + "." + fractionalstr if tzstr is None: tz = None else: tz = parse_timezone(tzstr, builder=TupleBuilder) return builder.build_time(hh=hourstr, mm=minutestr, ss=secondstr, tz=tz)
def parse_date(isodatestr, builder=PythonTimeBuilder): #Given a string in any ISO 8601 date format, return a datetime.date #object that corresponds to the given date. Valid string formats are: # #Y[YYY] #YYYY-MM-DD #YYYYMMDD #YYYY-MM #YYYY-Www #YYYYWww #YYYY-Www-D #YYYYWwwD #YYYY-DDD #YYYYDDD if is_string(isodatestr) is False: raise ValueError('Date must be string.') if isodatestr.startswith('+') or isodatestr.startswith('-'): raise NotImplementedError('ISO 8601 extended year representation ' 'not supported.') if len(isodatestr) == 0 or isodatestr.count('-') > 2: raise ISOFormatError( '"{0}" is not a valid ISO 8601 date.'.format(isodatestr)) yearstr = None monthstr = None daystr = None weekstr = None weekdaystr = None ordinaldaystr = None if len(isodatestr) <= 4: #Y[YYY] yearstr = isodatestr elif 'W' in isodatestr: if len(isodatestr) == 10: #YYYY-Www-D yearstr = isodatestr[0:4] weekstr = isodatestr[6:8] weekdaystr = isodatestr[9] elif len(isodatestr) == 8: if '-' in isodatestr: #YYYY-Www yearstr = isodatestr[0:4] weekstr = isodatestr[6:] else: #YYYYWwwD yearstr = isodatestr[0:4] weekstr = isodatestr[5:7] weekdaystr = isodatestr[7] elif len(isodatestr) == 7: #YYYYWww yearstr = isodatestr[0:4] weekstr = isodatestr[5:] elif len(isodatestr) == 7: if '-' in isodatestr: #YYYY-MM yearstr = isodatestr[0:4] monthstr = isodatestr[5:] else: #YYYYDDD yearstr = isodatestr[0:4] ordinaldaystr = isodatestr[4:] elif len(isodatestr) == 8: if '-' in isodatestr: #YYYY-DDD yearstr = isodatestr[0:4] ordinaldaystr = isodatestr[5:] else: #YYYYMMDD yearstr = isodatestr[0:4] monthstr = isodatestr[4:6] daystr = isodatestr[6:] elif len(isodatestr) == 10: #YYYY-MM-DD yearstr = isodatestr[0:4] monthstr = isodatestr[5:7] daystr = isodatestr[8:] else: raise ISOFormatError( '"{0}" is not a valid ISO 8601 date.'.format(isodatestr)) hascomponent = False for componentstr in [ yearstr, monthstr, daystr, weekstr, weekdaystr, ordinaldaystr ]: if componentstr is not None: hascomponent = True if componentstr.isdigit() is False: raise ISOFormatError( '"{0}" is not a valid ISO 8601 date.'.format(isodatestr)) if hascomponent is False: raise ISOFormatError( '"{0}" is not a valid ISO 8601 date.'.format(isodatestr)) return builder.build_date(YYYY=yearstr, MM=monthstr, DD=daystr, Www=weekstr, D=weekdaystr, DDD=ordinaldaystr)
def get_date_resolution(isodatestr): #Valid string formats are: # #Y[YYY] #YYYY-MM-DD #YYYYMMDD #YYYY-MM #YYYY-Www #YYYYWww #YYYY-Www-D #YYYYWwwD #YYYY-DDD #YYYYDDD if is_string(isodatestr) is False: raise ValueError('Date must be string.') if isodatestr.startswith('+') or isodatestr.startswith('-'): raise NotImplementedError('ISO 8601 extended year representation ' 'not supported.') if (len(isodatestr) == 0 or isodatestr[0].isdigit() is False or isodatestr[-1].isdigit() is False): raise ISOFormatError( '"{0}" is not a valid ISO 8601 date.'.format(isodatestr)) if isodatestr.find('W') != -1: #Handle ISO 8601 week date format hyphens_present = 1 if isodatestr.find('-') != -1 else 0 week_date_len = 7 + hyphens_present weekday_date_len = 8 + 2 * hyphens_present if len(isodatestr) == week_date_len: #YYYY-Www #YYYYWww return DateResolution.Week elif len(isodatestr) == weekday_date_len: #YYYY-Www-D #YYYYWwwD return DateResolution.Weekday else: raise ISOFormatError( '"{0}" is not a valid ISO 8601 week date.'.format(isodatestr)) #If the size of the string of 4 or less, #assume its a truncated year representation if len(isodatestr) <= 4: return DateResolution.Year #An ISO string may be a calendar represntation if: # 1) When split on a hyphen, the sizes of the parts are 4, 2, 2 or 4, 2 # 2) There are no hyphens, and the length is 8 datestrsplit = isodatestr.split('-') #Check case 1 if len(datestrsplit) == 2: if len(datestrsplit[0]) == 4 and len(datestrsplit[1]) == 2: return DateResolution.Month if len(datestrsplit) == 3: if (len(datestrsplit[0]) == 4 and len(datestrsplit[1]) == 2 and len(datestrsplit[2]) == 2): return DateResolution.Day #Check case 2 if len(isodatestr) == 8 and isodatestr.find('-') == -1: return DateResolution.Day #An ISO string may be a ordinal date representation if: # 1) When split on a hyphen, the sizes of the parts are 4, 3 # 2) There are no hyphens, and the length is 7 #Check case 1 if len(datestrsplit) == 2: if len(datestrsplit[0]) == 4 and len(datestrsplit[1]) == 3: return DateResolution.Ordinal #Check case 2 if len(isodatestr) == 7 and isodatestr.find('-') == -1: return DateResolution.Ordinal #None of the date representations match raise ISOFormatError('"{0}" is not an ISO 8601 date, perhaps it ' 'represents a time or datetime.'.format(isodatestr))