Spaces:
Sleeping
Sleeping
# -*- coding: utf-8 -*- | |
# Copyright (c) 2021, Brandon Nielsen | |
# All rights reserved. | |
# | |
# This software may be modified and distributed under the terms | |
# of the BSD license. See the LICENSE file for details. | |
from aniso8601 import compat | |
from aniso8601.builders import TupleBuilder | |
from aniso8601.builders.python import PythonTimeBuilder | |
from aniso8601.date import parse_date | |
from aniso8601.decimalfraction import normalize | |
from aniso8601.exceptions import ISOFormatError | |
from aniso8601.resolution import DurationResolution | |
from aniso8601.time import parse_time | |
def get_duration_resolution(isodurationstr): | |
# Valid string formats are: | |
# | |
# PnYnMnDTnHnMnS (or any reduced precision equivalent) | |
# PnW | |
# P<date>T<time> | |
isodurationtuple = parse_duration(isodurationstr, builder=TupleBuilder) | |
if isodurationtuple.TnS is not None: | |
return DurationResolution.Seconds | |
if isodurationtuple.TnM is not None: | |
return DurationResolution.Minutes | |
if isodurationtuple.TnH is not None: | |
return DurationResolution.Hours | |
if isodurationtuple.PnD is not None: | |
return DurationResolution.Days | |
if isodurationtuple.PnW is not None: | |
return DurationResolution.Weeks | |
if isodurationtuple.PnM is not None: | |
return DurationResolution.Months | |
return DurationResolution.Years | |
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 _parse_duration_prescribed(isodurationstr): | |
# durationstr can be of the form PnYnMnDTnHnMnS or PnW | |
# Make sure the end character is valid | |
# https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed | |
if isodurationstr[-1] not in ["Y", "M", "D", "H", "S", "W"]: | |
raise ISOFormatError("ISO 8601 duration must end with a valid " "character.") | |
# Make sure only the lowest order element has decimal precision | |
durationstr = normalize(isodurationstr) | |
if durationstr.count(".") > 1: | |
raise ISOFormatError( | |
"ISO 8601 allows only lowest order element to " "have a decimal fraction." | |
) | |
seperatoridx = durationstr.find(".") | |
if seperatoridx != -1: | |
remaining = durationstr[seperatoridx + 1 : -1] | |
# There should only ever be 1 letter after a decimal if there is more | |
# then one, the string is invalid | |
if remaining.isdigit() is False: | |
raise ISOFormatError( | |
"ISO 8601 duration must end with " "a single valid character." | |
) | |
# Do not allow W in combination with other designators | |
# https://bitbucket.org/nielsenb/aniso8601/issues/2/week-designators-should-not-be-combinable | |
if ( | |
durationstr.find("W") != -1 | |
and _has_any_component(durationstr, ["Y", "M", "D", "H", "S"]) is True | |
): | |
raise ISOFormatError( | |
"ISO 8601 week designators may not be combined " | |
"with other time designators." | |
) | |
# Parse the elements of the duration | |
if durationstr.find("T") == -1: | |
return _parse_duration_prescribed_notime(durationstr) | |
return _parse_duration_prescribed_time(durationstr) | |
def _parse_duration_prescribed_notime(isodurationstr): | |
# durationstr can be of the form PnYnMnD or PnW | |
durationstr = normalize(isodurationstr) | |
yearstr = None | |
monthstr = None | |
daystr = None | |
weekstr = None | |
weekidx = durationstr.find("W") | |
yearidx = durationstr.find("Y") | |
monthidx = durationstr.find("M") | |
dayidx = durationstr.find("D") | |
if weekidx != -1: | |
weekstr = durationstr[1:-1] | |
elif yearidx != -1 and monthidx != -1 and dayidx != -1: | |
yearstr = durationstr[1:yearidx] | |
monthstr = durationstr[yearidx + 1 : monthidx] | |
daystr = durationstr[monthidx + 1 : -1] | |
elif yearidx != -1 and monthidx != -1: | |
yearstr = durationstr[1:yearidx] | |
monthstr = durationstr[yearidx + 1 : monthidx] | |
elif yearidx != -1 and dayidx != -1: | |
yearstr = durationstr[1:yearidx] | |
daystr = durationstr[yearidx + 1 : dayidx] | |
elif monthidx != -1 and dayidx != -1: | |
monthstr = durationstr[1:monthidx] | |
daystr = durationstr[monthidx + 1 : -1] | |
elif yearidx != -1: | |
yearstr = durationstr[1:-1] | |
elif monthidx != -1: | |
monthstr = durationstr[1:-1] | |
elif dayidx != -1: | |
daystr = durationstr[1:-1] | |
else: | |
raise ISOFormatError( | |
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) | |
) | |
for componentstr in [yearstr, monthstr, daystr, weekstr]: | |
if componentstr is not None: | |
if "." in componentstr: | |
intstr, fractionalstr = componentstr.split(".", 1) | |
if intstr.isdigit() is False: | |
raise ISOFormatError( | |
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) | |
) | |
else: | |
if componentstr.isdigit() is False: | |
raise ISOFormatError( | |
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) | |
) | |
return {"PnY": yearstr, "PnM": monthstr, "PnW": weekstr, "PnD": daystr} | |
def _parse_duration_prescribed_time(isodurationstr): | |
# durationstr can be of the form PnYnMnDTnHnMnS | |
timeidx = isodurationstr.find("T") | |
datestr = isodurationstr[:timeidx] | |
timestr = normalize(isodurationstr[timeidx + 1 :]) | |
hourstr = None | |
minutestr = None | |
secondstr = None | |
houridx = timestr.find("H") | |
minuteidx = timestr.find("M") | |
secondidx = timestr.find("S") | |
if houridx != -1 and minuteidx != -1 and secondidx != -1: | |
hourstr = timestr[0:houridx] | |
minutestr = timestr[houridx + 1 : minuteidx] | |
secondstr = timestr[minuteidx + 1 : -1] | |
elif houridx != -1 and minuteidx != -1: | |
hourstr = timestr[0:houridx] | |
minutestr = timestr[houridx + 1 : minuteidx] | |
elif houridx != -1 and secondidx != -1: | |
hourstr = timestr[0:houridx] | |
secondstr = timestr[houridx + 1 : -1] | |
elif minuteidx != -1 and secondidx != -1: | |
minutestr = timestr[0:minuteidx] | |
secondstr = timestr[minuteidx + 1 : -1] | |
elif houridx != -1: | |
hourstr = timestr[0:-1] | |
elif minuteidx != -1: | |
minutestr = timestr[0:-1] | |
elif secondidx != -1: | |
secondstr = timestr[0:-1] | |
else: | |
raise ISOFormatError( | |
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) | |
) | |
for componentstr in [hourstr, minutestr, secondstr]: | |
if componentstr is not None: | |
if "." in componentstr: | |
intstr, fractionalstr = componentstr.split(".", 1) | |
if intstr.isdigit() is False: | |
raise ISOFormatError( | |
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) | |
) | |
else: | |
if componentstr.isdigit() is False: | |
raise ISOFormatError( | |
'"{0}" is not a valid ISO 8601 duration.'.format(isodurationstr) | |
) | |
# Parse any date components | |
durationdict = {"PnY": None, "PnM": None, "PnW": None, "PnD": None} | |
if len(datestr) > 1: | |
durationdict = _parse_duration_prescribed_notime(datestr) | |
durationdict.update({"TnH": hourstr, "TnM": minutestr, "TnS": secondstr}) | |
return durationdict | |
def _parse_duration_combined(durationstr): | |
# Period of the form P<date>T<time> | |
# Split the string in to its component parts | |
datepart, timepart = durationstr[1:].split("T", 1) # We skip the 'P' | |
datevalue = parse_date(datepart, builder=TupleBuilder) | |
timevalue = parse_time(timepart, builder=TupleBuilder) | |
return { | |
"PnY": datevalue.YYYY, | |
"PnM": datevalue.MM, | |
"PnD": datevalue.DD, | |
"TnH": timevalue.hh, | |
"TnM": timevalue.mm, | |
"TnS": timevalue.ss, | |
} | |
def _has_any_component(durationstr, components): | |
# Given a duration string, and a list of components, returns True | |
# if any of the listed components are present, False otherwise. | |
# | |
# For instance: | |
# durationstr = 'P1Y' | |
# components = ['Y', 'M'] | |
# | |
# returns True | |
# | |
# durationstr = 'P1Y' | |
# components = ['M', 'D'] | |
# | |
# returns False | |
for component in components: | |
if durationstr.find(component) != -1: | |
return True | |
return False | |