bazarr/libs/aniso8601/interval.py

351 lines
10 KiB
Python

# -*- 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)