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.
351 lines
10 KiB
351 lines
10 KiB
2 years ago
|
# -*- 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.builders import DatetimeTuple, DateTuple, TupleBuilder
|
||
|
from aniso8601.builders.python import PythonTimeBuilder
|
||
|
from aniso8601.compat import is_string
|
||
|
from aniso8601.date import parse_date
|
||
|
from aniso8601.duration import parse_duration
|
||
|
from aniso8601.exceptions import ISOFormatError
|
||
|
from aniso8601.resolution import IntervalResolution
|
||
|
from aniso8601.time import parse_datetime, parse_time
|
||
|
|
||
|
|
||
|
def get_interval_resolution(
|
||
|
isointervalstr, intervaldelimiter="/", datetimedelimiter="T"
|
||
|
):
|
||
|
isointervaltuple = parse_interval(
|
||
|
isointervalstr,
|
||
|
intervaldelimiter=intervaldelimiter,
|
||
|
datetimedelimiter=datetimedelimiter,
|
||
|
builder=TupleBuilder,
|
||
|
)
|
||
|
|
||
|
return _get_interval_resolution(isointervaltuple)
|
||
|
|
||
|
|
||
|
def get_repeating_interval_resolution(
|
||
|
isointervalstr, intervaldelimiter="/", datetimedelimiter="T"
|
||
|
):
|
||
|
repeatingintervaltuple = parse_repeating_interval(
|
||
|
isointervalstr,
|
||
|
intervaldelimiter=intervaldelimiter,
|
||
|
datetimedelimiter=datetimedelimiter,
|
||
|
builder=TupleBuilder,
|
||
|
)
|
||
|
|
||
|
return _get_interval_resolution(repeatingintervaltuple.interval)
|
||
|
|
||
|
|
||
|
def _get_interval_resolution(intervaltuple):
|
||
|
if intervaltuple.start is not None and intervaltuple.end is not None:
|
||
|
return max(
|
||
|
_get_interval_component_resolution(intervaltuple.start),
|
||
|
_get_interval_component_resolution(intervaltuple.end),
|
||
|
)
|
||
|
|
||
|
if intervaltuple.start is not None and intervaltuple.duration is not None:
|
||
|
return max(
|
||
|
_get_interval_component_resolution(intervaltuple.start),
|
||
|
_get_interval_component_resolution(intervaltuple.duration),
|
||
|
)
|
||
|
|
||
|
return max(
|
||
|
_get_interval_component_resolution(intervaltuple.end),
|
||
|
_get_interval_component_resolution(intervaltuple.duration),
|
||
|
)
|
||
|
|
||
|
|
||
|
def _get_interval_component_resolution(componenttuple):
|
||
|
if type(componenttuple) is DateTuple:
|
||
|
if componenttuple.DDD is not None:
|
||
|
# YYYY-DDD
|
||
|
# YYYYDDD
|
||
|
return IntervalResolution.Ordinal
|
||
|
|
||
|
if componenttuple.D is not None:
|
||
|
# YYYY-Www-D
|
||
|
# YYYYWwwD
|
||
|
return IntervalResolution.Weekday
|
||
|
|
||
|
if componenttuple.Www is not None:
|
||
|
# YYYY-Www
|
||
|
# YYYYWww
|
||
|
return IntervalResolution.Week
|
||
|
|
||
|
if componenttuple.DD is not None:
|
||
|
# YYYY-MM-DD
|
||
|
# YYYYMMDD
|
||
|
return IntervalResolution.Day
|
||
|
|
||
|
if componenttuple.MM is not None:
|
||
|
# YYYY-MM
|
||
|
return IntervalResolution.Month
|
||
|
|
||
|
# Y[YYY]
|
||
|
return IntervalResolution.Year
|
||
|
elif type(componenttuple) is DatetimeTuple:
|
||
|
# Datetime
|
||
|
if componenttuple.time.ss is not None:
|
||
|
return IntervalResolution.Seconds
|
||
|
|
||
|
if componenttuple.time.mm is not None:
|
||
|
return IntervalResolution.Minutes
|
||
|
|
||
|
return IntervalResolution.Hours
|
||
|
|
||
|
# Duration
|
||
|
if componenttuple.TnS is not None:
|
||
|
return IntervalResolution.Seconds
|
||
|
|
||
|
if componenttuple.TnM is not None:
|
||
|
return IntervalResolution.Minutes
|
||
|
|
||
|
if componenttuple.TnH is not None:
|
||
|
return IntervalResolution.Hours
|
||
|
|
||
|
if componenttuple.PnD is not None:
|
||
|
return IntervalResolution.Day
|
||
|
|
||
|
if componenttuple.PnW is not None:
|
||
|
return IntervalResolution.Week
|
||
|
|
||
|
if componenttuple.PnM is not None:
|
||
|
return IntervalResolution.Month
|
||
|
|
||
|
return IntervalResolution.Year
|
||
|
|
||
|
|
||
|
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_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 not isinstance(isointervalstr, str):
|
||
|
raise ValueError("Interval must be string.")
|
||
|
|
||
|
if len(isointervalstr) == 0:
|
||
|
raise ISOFormatError("Repeating interval string is empty.")
|
||
|
|
||
|
if isointervalstr[0] != "R":
|
||
|
raise ISOFormatError("ISO 8601 repeating interval must start " "with an R.")
|
||
|
|
||
|
if intervaldelimiter not in isointervalstr:
|
||
|
raise ISOFormatError(
|
||
|
'Interval delimiter "{0}" is not in interval '
|
||
|
'string "{1}".'.format(intervaldelimiter, isointervalstr)
|
||
|
)
|
||
|
|
||
|
# 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_interval(
|
||
|
isointervalstr, builder, intervaldelimiter="/", datetimedelimiter="T"
|
||
|
):
|
||
|
# Returns a tuple containing the start of the interval, the end of the
|
||
|
# interval, and or the interval duration
|
||
|
|
||
|
firstpart, secondpart = isointervalstr.split(intervaldelimiter)
|
||
|
|
||
|
if len(firstpart) == 0 or len(secondpart) == 0:
|
||
|
raise ISOFormatError(
|
||
|
"{0} is not a valid ISO 8601 interval".format(isointervalstr)
|
||
|
)
|
||
|
|
||
|
if firstpart[0] == "P":
|
||
|
# <duration>/<end>
|
||
|
# Notice that these are not returned 'in order' (earlier to later), this
|
||
|
# is to maintain consistency with parsing <start>/<end> durations, as
|
||
|
# well as making repeating interval code cleaner. Users who desire
|
||
|
# durations to be in order can use the 'sorted' operator.
|
||
|
duration = parse_duration(firstpart, builder=TupleBuilder)
|
||
|
|
||
|
# We need to figure out if <end> is a date, or a datetime
|
||
|
if secondpart.find(datetimedelimiter) != -1:
|
||
|
# <end> is a datetime
|
||
|
endtuple = parse_datetime(
|
||
|
secondpart, delimiter=datetimedelimiter, builder=TupleBuilder
|
||
|
)
|
||
|
else:
|
||
|
endtuple = parse_date(secondpart, builder=TupleBuilder)
|
||
|
|
||
|
return builder.build_interval(end=endtuple, duration=duration)
|
||
|
elif secondpart[0] == "P":
|
||
|
# <start>/<duration>
|
||
|
# We need to figure out if <start> is a date, or a datetime
|
||
|
duration = parse_duration(secondpart, builder=TupleBuilder)
|
||
|
|
||
|
if firstpart.find(datetimedelimiter) != -1:
|
||
|
# <start> is a datetime
|
||
|
starttuple = parse_datetime(
|
||
|
firstpart, delimiter=datetimedelimiter, builder=TupleBuilder
|
||
|
)
|
||
|
else:
|
||
|
# <start> must just be a date
|
||
|
starttuple = parse_date(firstpart, builder=TupleBuilder)
|
||
|
|
||
|
return builder.build_interval(start=starttuple, duration=duration)
|
||
|
|
||
|
# <start>/<end>
|
||
|
if firstpart.find(datetimedelimiter) != -1:
|
||
|
# Both parts are datetimes
|
||
|
starttuple = parse_datetime(
|
||
|
firstpart, delimiter=datetimedelimiter, builder=TupleBuilder
|
||
|
)
|
||
|
else:
|
||
|
starttuple = parse_date(firstpart, builder=TupleBuilder)
|
||
|
|
||
|
endtuple = _parse_interval_end(secondpart, starttuple, datetimedelimiter)
|
||
|
|
||
|
return builder.build_interval(start=starttuple, end=endtuple)
|
||
|
|
||
|
|
||
|
def _parse_interval_end(endstr, starttuple, datetimedelimiter):
|
||
|
datestr = None
|
||
|
timestr = None
|
||
|
|
||
|
monthstr = None
|
||
|
daystr = None
|
||
|
|
||
|
concise = False
|
||
|
|
||
|
if type(starttuple) is DateTuple:
|
||
|
startdatetuple = starttuple
|
||
|
else:
|
||
|
# Start is a datetime
|
||
|
startdatetuple = starttuple.date
|
||
|
|
||
|
if datetimedelimiter in endstr:
|
||
|
datestr, timestr = endstr.split(datetimedelimiter, 1)
|
||
|
elif ":" in endstr:
|
||
|
timestr = endstr
|
||
|
else:
|
||
|
datestr = endstr
|
||
|
|
||
|
if timestr is not None:
|
||
|
endtimetuple = parse_time(timestr, builder=TupleBuilder)
|
||
|
|
||
|
# End is just a time
|
||
|
if datestr is None:
|
||
|
return endtimetuple
|
||
|
|
||
|
# Handle backwards concise representation
|
||
|
if datestr.count("-") == 1:
|
||
|
monthstr, daystr = datestr.split("-")
|
||
|
concise = True
|
||
|
elif len(datestr) <= 2:
|
||
|
daystr = datestr
|
||
|
concise = True
|
||
|
elif len(datestr) <= 4:
|
||
|
monthstr = datestr[0:2]
|
||
|
daystr = datestr[2:]
|
||
|
concise = True
|
||
|
|
||
|
if concise is True:
|
||
|
concisedatestr = startdatetuple.YYYY
|
||
|
|
||
|
# Separators required because concise elements may be missing digits
|
||
|
if monthstr is not None:
|
||
|
concisedatestr += "-" + monthstr
|
||
|
elif startdatetuple.MM is not None:
|
||
|
concisedatestr += "-" + startdatetuple.MM
|
||
|
|
||
|
concisedatestr += "-" + daystr
|
||
|
|
||
|
enddatetuple = parse_date(concisedatestr, builder=TupleBuilder)
|
||
|
|
||
|
# Clear unsupplied components
|
||
|
if monthstr is None:
|
||
|
enddatetuple = TupleBuilder.build_date(DD=enddatetuple.DD)
|
||
|
else:
|
||
|
# Year not provided
|
||
|
enddatetuple = TupleBuilder.build_date(
|
||
|
MM=enddatetuple.MM, DD=enddatetuple.DD
|
||
|
)
|
||
|
else:
|
||
|
enddatetuple = parse_date(datestr, builder=TupleBuilder)
|
||
|
|
||
|
if timestr is None:
|
||
|
return enddatetuple
|
||
|
|
||
|
return TupleBuilder.build_datetime(enddatetuple, endtimetuple)
|