You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
9.4 KiB
292 lines
9.4 KiB
# -*- 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
|