summaryrefslogtreecommitdiffhomepage
path: root/libs/aniso8601
diff options
context:
space:
mode:
authormorpheus65535 <[email protected]>2022-09-22 06:33:33 -0400
committermorpheus65535 <[email protected]>2022-09-22 06:33:33 -0400
commita338de147e8a4d74ca266b1306997fcfc90b8941 (patch)
tree6970a11c1c6126677f00e153107c2812a37f069a /libs/aniso8601
parent131b4e5cde4034f78923d7eaebd49b3550f8aa13 (diff)
downloadbazarr-a338de147e8a4d74ca266b1306997fcfc90b8941.tar.gz
bazarr-a338de147e8a4d74ca266b1306997fcfc90b8941.zip
Fixed import error after last commit.v1.1.2-beta.8
Diffstat (limited to 'libs/aniso8601')
-rw-r--r--libs/aniso8601/__init__.py26
-rw-r--r--libs/aniso8601/builders/__init__.py614
-rw-r--r--libs/aniso8601/builders/python.py705
-rw-r--r--libs/aniso8601/builders/tests/__init__.py7
-rw-r--r--libs/aniso8601/builders/tests/test_init.py838
-rw-r--r--libs/aniso8601/builders/tests/test_python.py1710
-rw-r--r--libs/aniso8601/compat.py24
-rw-r--r--libs/aniso8601/date.py161
-rw-r--r--libs/aniso8601/decimalfraction.py12
-rw-r--r--libs/aniso8601/duration.py291
-rw-r--r--libs/aniso8601/exceptions.py51
-rw-r--r--libs/aniso8601/interval.py350
-rw-r--r--libs/aniso8601/resolution.py27
-rw-r--r--libs/aniso8601/tests/__init__.py7
-rw-r--r--libs/aniso8601/tests/compat.py16
-rw-r--r--libs/aniso8601/tests/test_compat.py27
-rw-r--r--libs/aniso8601/tests/test_date.py303
-rw-r--r--libs/aniso8601/tests/test_decimalfraction.py19
-rw-r--r--libs/aniso8601/tests/test_duration.py1402
-rw-r--r--libs/aniso8601/tests/test_init.py49
-rw-r--r--libs/aniso8601/tests/test_interval.py1675
-rw-r--r--libs/aniso8601/tests/test_time.py539
-rw-r--r--libs/aniso8601/tests/test_timezone.py123
-rw-r--r--libs/aniso8601/tests/test_utcoffset.py56
-rw-r--r--libs/aniso8601/time.py203
-rw-r--r--libs/aniso8601/timezone.py62
-rw-r--r--libs/aniso8601/utcoffset.py71
27 files changed, 9368 insertions, 0 deletions
diff --git a/libs/aniso8601/__init__.py b/libs/aniso8601/__init__.py
new file mode 100644
index 000000000..033d30b9d
--- /dev/null
+++ b/libs/aniso8601/__init__.py
@@ -0,0 +1,26 @@
+# -*- 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.date import get_date_resolution, parse_date
+from aniso8601.duration import get_duration_resolution, parse_duration
+from aniso8601.interval import (
+ get_interval_resolution,
+ get_repeating_interval_resolution,
+ parse_interval,
+ parse_repeating_interval,
+)
+
+# Import the main parsing functions so they are readily available
+from aniso8601.time import (
+ get_datetime_resolution,
+ get_time_resolution,
+ parse_datetime,
+ parse_time,
+)
+
+__version__ = "9.0.1"
diff --git a/libs/aniso8601/builders/__init__.py b/libs/aniso8601/builders/__init__.py
new file mode 100644
index 000000000..834c72a6b
--- /dev/null
+++ b/libs/aniso8601/builders/__init__.py
@@ -0,0 +1,614 @@
+# -*- 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.
+
+import calendar
+from collections import namedtuple
+
+from aniso8601.exceptions import (
+ DayOutOfBoundsError,
+ HoursOutOfBoundsError,
+ ISOFormatError,
+ LeapSecondError,
+ MidnightBoundsError,
+ MinutesOutOfBoundsError,
+ MonthOutOfBoundsError,
+ SecondsOutOfBoundsError,
+ WeekOutOfBoundsError,
+ YearOutOfBoundsError,
+)
+
+DateTuple = namedtuple("Date", ["YYYY", "MM", "DD", "Www", "D", "DDD"])
+TimeTuple = namedtuple("Time", ["hh", "mm", "ss", "tz"])
+DatetimeTuple = namedtuple("Datetime", ["date", "time"])
+DurationTuple = namedtuple(
+ "Duration", ["PnY", "PnM", "PnW", "PnD", "TnH", "TnM", "TnS"]
+)
+IntervalTuple = namedtuple("Interval", ["start", "end", "duration"])
+RepeatingIntervalTuple = namedtuple("RepeatingInterval", ["R", "Rnn", "interval"])
+TimezoneTuple = namedtuple("Timezone", ["negative", "Z", "hh", "mm", "name"])
+
+Limit = namedtuple(
+ "Limit",
+ [
+ "casterrorstring",
+ "min",
+ "max",
+ "rangeexception",
+ "rangeerrorstring",
+ "rangefunc",
+ ],
+)
+
+
+def cast(
+ value,
+ castfunction,
+ caughtexceptions=(ValueError,),
+ thrownexception=ISOFormatError,
+ thrownmessage=None,
+):
+ try:
+ result = castfunction(value)
+ except caughtexceptions:
+ raise thrownexception(thrownmessage)
+
+ return result
+
+
+def range_check(valuestr, limit):
+ # Returns cast value if in range, raises defined exceptions on failure
+ if valuestr is None:
+ return None
+
+ if "." in valuestr:
+ castfunc = float
+ else:
+ castfunc = int
+
+ value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring)
+
+ if limit.min is not None and value < limit.min:
+ raise limit.rangeexception(limit.rangeerrorstring)
+
+ if limit.max is not None and value > limit.max:
+ raise limit.rangeexception(limit.rangeerrorstring)
+
+ return value
+
+
+class BaseTimeBuilder(object):
+ # Limit tuple format cast function, cast error string,
+ # lower limit, upper limit, limit error string
+ DATE_YYYY_LIMIT = Limit(
+ "Invalid year string.",
+ 0000,
+ 9999,
+ YearOutOfBoundsError,
+ "Year must be between 1..9999.",
+ range_check,
+ )
+ DATE_MM_LIMIT = Limit(
+ "Invalid month string.",
+ 1,
+ 12,
+ MonthOutOfBoundsError,
+ "Month must be between 1..12.",
+ range_check,
+ )
+ DATE_DD_LIMIT = Limit(
+ "Invalid day string.",
+ 1,
+ 31,
+ DayOutOfBoundsError,
+ "Day must be between 1..31.",
+ range_check,
+ )
+ DATE_WWW_LIMIT = Limit(
+ "Invalid week string.",
+ 1,
+ 53,
+ WeekOutOfBoundsError,
+ "Week number must be between 1..53.",
+ range_check,
+ )
+ DATE_D_LIMIT = Limit(
+ "Invalid weekday string.",
+ 1,
+ 7,
+ DayOutOfBoundsError,
+ "Weekday number must be between 1..7.",
+ range_check,
+ )
+ DATE_DDD_LIMIT = Limit(
+ "Invalid ordinal day string.",
+ 1,
+ 366,
+ DayOutOfBoundsError,
+ "Ordinal day must be between 1..366.",
+ range_check,
+ )
+ TIME_HH_LIMIT = Limit(
+ "Invalid hour string.",
+ 0,
+ 24,
+ HoursOutOfBoundsError,
+ "Hour must be between 0..24 with " "24 representing midnight.",
+ range_check,
+ )
+ TIME_MM_LIMIT = Limit(
+ "Invalid minute string.",
+ 0,
+ 59,
+ MinutesOutOfBoundsError,
+ "Minute must be between 0..59.",
+ range_check,
+ )
+ TIME_SS_LIMIT = Limit(
+ "Invalid second string.",
+ 0,
+ 60,
+ SecondsOutOfBoundsError,
+ "Second must be between 0..60 with " "60 representing a leap second.",
+ range_check,
+ )
+ TZ_HH_LIMIT = Limit(
+ "Invalid timezone hour string.",
+ 0,
+ 23,
+ HoursOutOfBoundsError,
+ "Hour must be between 0..23.",
+ range_check,
+ )
+ TZ_MM_LIMIT = Limit(
+ "Invalid timezone minute string.",
+ 0,
+ 59,
+ MinutesOutOfBoundsError,
+ "Minute must be between 0..59.",
+ range_check,
+ )
+ DURATION_PNY_LIMIT = Limit(
+ "Invalid year duration string.",
+ 0,
+ None,
+ ISOFormatError,
+ "Duration years component must be positive.",
+ range_check,
+ )
+ DURATION_PNM_LIMIT = Limit(
+ "Invalid month duration string.",
+ 0,
+ None,
+ ISOFormatError,
+ "Duration months component must be positive.",
+ range_check,
+ )
+ DURATION_PNW_LIMIT = Limit(
+ "Invalid week duration string.",
+ 0,
+ None,
+ ISOFormatError,
+ "Duration weeks component must be positive.",
+ range_check,
+ )
+ DURATION_PND_LIMIT = Limit(
+ "Invalid day duration string.",
+ 0,
+ None,
+ ISOFormatError,
+ "Duration days component must be positive.",
+ range_check,
+ )
+ DURATION_TNH_LIMIT = Limit(
+ "Invalid hour duration string.",
+ 0,
+ None,
+ ISOFormatError,
+ "Duration hours component must be positive.",
+ range_check,
+ )
+ DURATION_TNM_LIMIT = Limit(
+ "Invalid minute duration string.",
+ 0,
+ None,
+ ISOFormatError,
+ "Duration minutes component must be positive.",
+ range_check,
+ )
+ DURATION_TNS_LIMIT = Limit(
+ "Invalid second duration string.",
+ 0,
+ None,
+ ISOFormatError,
+ "Duration seconds component must be positive.",
+ range_check,
+ )
+ INTERVAL_RNN_LIMIT = Limit(
+ "Invalid duration repetition string.",
+ 0,
+ None,
+ ISOFormatError,
+ "Duration repetition count must be positive.",
+ range_check,
+ )
+
+ DATE_RANGE_DICT = {
+ "YYYY": DATE_YYYY_LIMIT,
+ "MM": DATE_MM_LIMIT,
+ "DD": DATE_DD_LIMIT,
+ "Www": DATE_WWW_LIMIT,
+ "D": DATE_D_LIMIT,
+ "DDD": DATE_DDD_LIMIT,
+ }
+
+ TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT}
+
+ DURATION_RANGE_DICT = {
+ "PnY": DURATION_PNY_LIMIT,
+ "PnM": DURATION_PNM_LIMIT,
+ "PnW": DURATION_PNW_LIMIT,
+ "PnD": DURATION_PND_LIMIT,
+ "TnH": DURATION_TNH_LIMIT,
+ "TnM": DURATION_TNM_LIMIT,
+ "TnS": DURATION_TNS_LIMIT,
+ }
+
+ REPEATING_INTERVAL_RANGE_DICT = {"Rnn": INTERVAL_RNN_LIMIT}
+
+ TIMEZONE_RANGE_DICT = {"hh": TZ_HH_LIMIT, "mm": TZ_MM_LIMIT}
+
+ LEAP_SECONDS_SUPPORTED = False
+
+ @classmethod
+ def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None):
+ raise NotImplementedError
+
+ @classmethod
+ def build_time(cls, hh=None, mm=None, ss=None, tz=None):
+ raise NotImplementedError
+
+ @classmethod
+ def build_datetime(cls, date, time):
+ raise NotImplementedError
+
+ @classmethod
+ def build_duration(
+ cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
+ ):
+ raise NotImplementedError
+
+ @classmethod
+ def build_interval(cls, start=None, end=None, duration=None):
+ # start, end, and duration are all tuples
+ raise NotImplementedError
+
+ @classmethod
+ def build_repeating_interval(cls, R=None, Rnn=None, interval=None):
+ # interval is a tuple
+ raise NotImplementedError
+
+ @classmethod
+ def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""):
+ raise NotImplementedError
+
+ @classmethod
+ def range_check_date(
+ cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None, rangedict=None
+ ):
+ if rangedict is None:
+ rangedict = cls.DATE_RANGE_DICT
+
+ if "YYYY" in rangedict:
+ YYYY = rangedict["YYYY"].rangefunc(YYYY, rangedict["YYYY"])
+
+ if "MM" in rangedict:
+ MM = rangedict["MM"].rangefunc(MM, rangedict["MM"])
+
+ if "DD" in rangedict:
+ DD = rangedict["DD"].rangefunc(DD, rangedict["DD"])
+
+ if "Www" in rangedict:
+ Www = rangedict["Www"].rangefunc(Www, rangedict["Www"])
+
+ if "D" in rangedict:
+ D = rangedict["D"].rangefunc(D, rangedict["D"])
+
+ if "DDD" in rangedict:
+ DDD = rangedict["DDD"].rangefunc(DDD, rangedict["DDD"])
+
+ if DD is not None:
+ # Check calendar
+ if DD > calendar.monthrange(YYYY, MM)[1]:
+ raise DayOutOfBoundsError(
+ "{0} is out of range for {1}-{2}".format(DD, YYYY, MM)
+ )
+
+ if DDD is not None:
+ if calendar.isleap(YYYY) is False and DDD == 366:
+ raise DayOutOfBoundsError(
+ "{0} is only valid for leap year.".format(DDD)
+ )
+
+ return (YYYY, MM, DD, Www, D, DDD)
+
+ @classmethod
+ def range_check_time(cls, hh=None, mm=None, ss=None, tz=None, rangedict=None):
+ # Used for midnight and leap second handling
+ midnight = False # Handle hh = '24' specially
+
+ if rangedict is None:
+ rangedict = cls.TIME_RANGE_DICT
+
+ if "hh" in rangedict:
+ try:
+ hh = rangedict["hh"].rangefunc(hh, rangedict["hh"])
+ except HoursOutOfBoundsError as e:
+ if float(hh) > 24 and float(hh) < 25:
+ raise MidnightBoundsError("Hour 24 may only represent midnight.")
+
+ raise e
+
+ if "mm" in rangedict:
+ mm = rangedict["mm"].rangefunc(mm, rangedict["mm"])
+
+ if "ss" in rangedict:
+ ss = rangedict["ss"].rangefunc(ss, rangedict["ss"])
+
+ if hh is not None and hh == 24:
+ midnight = True
+
+ # Handle midnight range
+ if midnight is True and (
+ (mm is not None and mm != 0) or (ss is not None and ss != 0)
+ ):
+ raise MidnightBoundsError("Hour 24 may only represent midnight.")
+
+ if cls.LEAP_SECONDS_SUPPORTED is True:
+ if hh != 23 and mm != 59 and ss == 60:
+ raise cls.TIME_SS_LIMIT.rangeexception(
+ cls.TIME_SS_LIMIT.rangeerrorstring
+ )
+ else:
+ if hh == 23 and mm == 59 and ss == 60:
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ raise LeapSecondError("Leap seconds are not supported.")
+
+ if ss == 60:
+ raise cls.TIME_SS_LIMIT.rangeexception(
+ cls.TIME_SS_LIMIT.rangeerrorstring
+ )
+
+ return (hh, mm, ss, tz)
+
+ @classmethod
+ def range_check_duration(
+ cls,
+ PnY=None,
+ PnM=None,
+ PnW=None,
+ PnD=None,
+ TnH=None,
+ TnM=None,
+ TnS=None,
+ rangedict=None,
+ ):
+ if rangedict is None:
+ rangedict = cls.DURATION_RANGE_DICT
+
+ if "PnY" in rangedict:
+ PnY = rangedict["PnY"].rangefunc(PnY, rangedict["PnY"])
+
+ if "PnM" in rangedict:
+ PnM = rangedict["PnM"].rangefunc(PnM, rangedict["PnM"])
+
+ if "PnW" in rangedict:
+ PnW = rangedict["PnW"].rangefunc(PnW, rangedict["PnW"])
+
+ if "PnD" in rangedict:
+ PnD = rangedict["PnD"].rangefunc(PnD, rangedict["PnD"])
+
+ if "TnH" in rangedict:
+ TnH = rangedict["TnH"].rangefunc(TnH, rangedict["TnH"])
+
+ if "TnM" in rangedict:
+ TnM = rangedict["TnM"].rangefunc(TnM, rangedict["TnM"])
+
+ if "TnS" in rangedict:
+ TnS = rangedict["TnS"].rangefunc(TnS, rangedict["TnS"])
+
+ return (PnY, PnM, PnW, PnD, TnH, TnM, TnS)
+
+ @classmethod
+ def range_check_repeating_interval(
+ cls, R=None, Rnn=None, interval=None, rangedict=None
+ ):
+ if rangedict is None:
+ rangedict = cls.REPEATING_INTERVAL_RANGE_DICT
+
+ if "Rnn" in rangedict:
+ Rnn = rangedict["Rnn"].rangefunc(Rnn, rangedict["Rnn"])
+
+ return (R, Rnn, interval)
+
+ @classmethod
+ def range_check_timezone(
+ cls, negative=None, Z=None, hh=None, mm=None, name="", rangedict=None
+ ):
+ if rangedict is None:
+ rangedict = cls.TIMEZONE_RANGE_DICT
+
+ if "hh" in rangedict:
+ hh = rangedict["hh"].rangefunc(hh, rangedict["hh"])
+
+ if "mm" in rangedict:
+ mm = rangedict["mm"].rangefunc(mm, rangedict["mm"])
+
+ return (negative, Z, hh, mm, name)
+
+ @classmethod
+ def _build_object(cls, parsetuple):
+ # Given a TupleBuilder tuple, build the correct object
+ if type(parsetuple) is DateTuple:
+ return cls.build_date(
+ YYYY=parsetuple.YYYY,
+ MM=parsetuple.MM,
+ DD=parsetuple.DD,
+ Www=parsetuple.Www,
+ D=parsetuple.D,
+ DDD=parsetuple.DDD,
+ )
+
+ if type(parsetuple) is TimeTuple:
+ return cls.build_time(
+ hh=parsetuple.hh, mm=parsetuple.mm, ss=parsetuple.ss, tz=parsetuple.tz
+ )
+
+ if type(parsetuple) is DatetimeTuple:
+ return cls.build_datetime(parsetuple.date, parsetuple.time)
+
+ if type(parsetuple) is DurationTuple:
+ return cls.build_duration(
+ PnY=parsetuple.PnY,
+ PnM=parsetuple.PnM,
+ PnW=parsetuple.PnW,
+ PnD=parsetuple.PnD,
+ TnH=parsetuple.TnH,
+ TnM=parsetuple.TnM,
+ TnS=parsetuple.TnS,
+ )
+
+ if type(parsetuple) is IntervalTuple:
+ return cls.build_interval(
+ start=parsetuple.start, end=parsetuple.end, duration=parsetuple.duration
+ )
+
+ if type(parsetuple) is RepeatingIntervalTuple:
+ return cls.build_repeating_interval(
+ R=parsetuple.R, Rnn=parsetuple.Rnn, interval=parsetuple.interval
+ )
+
+ return cls.build_timezone(
+ negative=parsetuple.negative,
+ Z=parsetuple.Z,
+ hh=parsetuple.hh,
+ mm=parsetuple.mm,
+ name=parsetuple.name,
+ )
+
+ @classmethod
+ def _is_interval_end_concise(cls, endtuple):
+ if type(endtuple) is TimeTuple:
+ return True
+
+ if type(endtuple) is DatetimeTuple:
+ enddatetuple = endtuple.date
+ else:
+ enddatetuple = endtuple
+
+ if enddatetuple.YYYY is None:
+ return True
+
+ return False
+
+ @classmethod
+ def _combine_concise_interval_tuples(cls, starttuple, conciseendtuple):
+ starttimetuple = None
+ startdatetuple = None
+
+ endtimetuple = None
+ enddatetuple = None
+
+ if type(starttuple) is DateTuple:
+ startdatetuple = starttuple
+ else:
+ # Start is a datetime
+ starttimetuple = starttuple.time
+ startdatetuple = starttuple.date
+
+ if type(conciseendtuple) is DateTuple:
+ enddatetuple = conciseendtuple
+ elif type(conciseendtuple) is DatetimeTuple:
+ enddatetuple = conciseendtuple.date
+ endtimetuple = conciseendtuple.time
+ else:
+ # Time
+ endtimetuple = conciseendtuple
+
+ if enddatetuple is not None:
+ if enddatetuple.YYYY is None and enddatetuple.MM is None:
+ newenddatetuple = DateTuple(
+ YYYY=startdatetuple.YYYY,
+ MM=startdatetuple.MM,
+ DD=enddatetuple.DD,
+ Www=enddatetuple.Www,
+ D=enddatetuple.D,
+ DDD=enddatetuple.DDD,
+ )
+ else:
+ newenddatetuple = DateTuple(
+ YYYY=startdatetuple.YYYY,
+ MM=enddatetuple.MM,
+ DD=enddatetuple.DD,
+ Www=enddatetuple.Www,
+ D=enddatetuple.D,
+ DDD=enddatetuple.DDD,
+ )
+
+ if (starttimetuple is not None and starttimetuple.tz is not None) and (
+ endtimetuple is not None and endtimetuple.tz != starttimetuple.tz
+ ):
+ # Copy the timezone across
+ endtimetuple = TimeTuple(
+ hh=endtimetuple.hh,
+ mm=endtimetuple.mm,
+ ss=endtimetuple.ss,
+ tz=starttimetuple.tz,
+ )
+
+ if enddatetuple is not None and endtimetuple is None:
+ return newenddatetuple
+
+ if enddatetuple is not None and endtimetuple is not None:
+ return TupleBuilder.build_datetime(newenddatetuple, endtimetuple)
+
+ return TupleBuilder.build_datetime(startdatetuple, endtimetuple)
+
+
+class TupleBuilder(BaseTimeBuilder):
+ # Builder used to return the arguments as a tuple, cleans up some parse methods
+ @classmethod
+ def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None):
+
+ return DateTuple(YYYY, MM, DD, Www, D, DDD)
+
+ @classmethod
+ def build_time(cls, hh=None, mm=None, ss=None, tz=None):
+ return TimeTuple(hh, mm, ss, tz)
+
+ @classmethod
+ def build_datetime(cls, date, time):
+ return DatetimeTuple(date, time)
+
+ @classmethod
+ def build_duration(
+ cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
+ ):
+
+ return DurationTuple(PnY, PnM, PnW, PnD, TnH, TnM, TnS)
+
+ @classmethod
+ def build_interval(cls, start=None, end=None, duration=None):
+ return IntervalTuple(start, end, duration)
+
+ @classmethod
+ def build_repeating_interval(cls, R=None, Rnn=None, interval=None):
+ return RepeatingIntervalTuple(R, Rnn, interval)
+
+ @classmethod
+ def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""):
+ return TimezoneTuple(negative, Z, hh, mm, name)
diff --git a/libs/aniso8601/builders/python.py b/libs/aniso8601/builders/python.py
new file mode 100644
index 000000000..8956740e7
--- /dev/null
+++ b/libs/aniso8601/builders/python.py
@@ -0,0 +1,705 @@
+# -*- 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.
+
+import datetime
+from collections import namedtuple
+from functools import partial
+
+from aniso8601.builders import (
+ BaseTimeBuilder,
+ DatetimeTuple,
+ DateTuple,
+ Limit,
+ TimeTuple,
+ TupleBuilder,
+ cast,
+ range_check,
+)
+from aniso8601.exceptions import (
+ DayOutOfBoundsError,
+ HoursOutOfBoundsError,
+ ISOFormatError,
+ LeapSecondError,
+ MidnightBoundsError,
+ MinutesOutOfBoundsError,
+ MonthOutOfBoundsError,
+ SecondsOutOfBoundsError,
+ WeekOutOfBoundsError,
+ YearOutOfBoundsError,
+)
+from aniso8601.utcoffset import UTCOffset
+
+DAYS_PER_YEAR = 365
+DAYS_PER_MONTH = 30
+DAYS_PER_WEEK = 7
+
+HOURS_PER_DAY = 24
+
+MINUTES_PER_HOUR = 60
+MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY
+
+SECONDS_PER_MINUTE = 60
+SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE
+
+MICROSECONDS_PER_SECOND = int(1e6)
+
+MICROSECONDS_PER_MINUTE = 60 * MICROSECONDS_PER_SECOND
+MICROSECONDS_PER_HOUR = 60 * MICROSECONDS_PER_MINUTE
+MICROSECONDS_PER_DAY = 24 * MICROSECONDS_PER_HOUR
+MICROSECONDS_PER_WEEK = 7 * MICROSECONDS_PER_DAY
+MICROSECONDS_PER_MONTH = DAYS_PER_MONTH * MICROSECONDS_PER_DAY
+MICROSECONDS_PER_YEAR = DAYS_PER_YEAR * MICROSECONDS_PER_DAY
+
+TIMEDELTA_MAX_DAYS = datetime.timedelta.max.days
+
+FractionalComponent = namedtuple(
+ "FractionalComponent", ["principal", "microsecondremainder"]
+)
+
+
+def year_range_check(valuestr, limit):
+ YYYYstr = valuestr
+
+ # Truncated dates, like '19', refer to 1900-1999 inclusive,
+ # we simply parse to 1900
+ if len(valuestr) < 4:
+ # Shift 0s in from the left to form complete year
+ YYYYstr = valuestr.ljust(4, "0")
+
+ return range_check(YYYYstr, limit)
+
+
+def fractional_range_check(conversion, valuestr, limit):
+ if valuestr is None:
+ return None
+
+ if "." in valuestr:
+ castfunc = partial(_cast_to_fractional_component, conversion)
+ else:
+ castfunc = int
+
+ value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring)
+
+ if type(value) is FractionalComponent:
+ tocheck = float(valuestr)
+ else:
+ tocheck = int(valuestr)
+
+ if limit.min is not None and tocheck < limit.min:
+ raise limit.rangeexception(limit.rangeerrorstring)
+
+ if limit.max is not None and tocheck > limit.max:
+ raise limit.rangeexception(limit.rangeerrorstring)
+
+ return value
+
+
+def _cast_to_fractional_component(conversion, floatstr):
+ # Splits a string with a decimal point into an int, and
+ # int representing the floating point remainder as a number
+ # of microseconds, determined by multiplying by conversion
+ intpart, floatpart = floatstr.split(".")
+
+ intvalue = int(intpart)
+ preconvertedvalue = int(floatpart)
+
+ convertedvalue = (preconvertedvalue * conversion) // (10 ** len(floatpart))
+
+ return FractionalComponent(intvalue, convertedvalue)
+
+
+class PythonTimeBuilder(BaseTimeBuilder):
+ # 0000 (1 BC) is not representable as a Python date
+ DATE_YYYY_LIMIT = Limit(
+ "Invalid year string.",
+ datetime.MINYEAR,
+ datetime.MAXYEAR,
+ YearOutOfBoundsError,
+ "Year must be between {0}..{1}.".format(datetime.MINYEAR, datetime.MAXYEAR),
+ year_range_check,
+ )
+ TIME_HH_LIMIT = Limit(
+ "Invalid hour string.",
+ 0,
+ 24,
+ HoursOutOfBoundsError,
+ "Hour must be between 0..24 with " "24 representing midnight.",
+ partial(fractional_range_check, MICROSECONDS_PER_HOUR),
+ )
+ TIME_MM_LIMIT = Limit(
+ "Invalid minute string.",
+ 0,
+ 59,
+ MinutesOutOfBoundsError,
+ "Minute must be between 0..59.",
+ partial(fractional_range_check, MICROSECONDS_PER_MINUTE),
+ )
+ TIME_SS_LIMIT = Limit(
+ "Invalid second string.",
+ 0,
+ 60,
+ SecondsOutOfBoundsError,
+ "Second must be between 0..60 with " "60 representing a leap second.",
+ partial(fractional_range_check, MICROSECONDS_PER_SECOND),
+ )
+ DURATION_PNY_LIMIT = Limit(
+ "Invalid year duration string.",
+ None,
+ None,
+ YearOutOfBoundsError,
+ None,
+ partial(fractional_range_check, MICROSECONDS_PER_YEAR),
+ )
+ DURATION_PNM_LIMIT = Limit(
+ "Invalid month duration string.",
+ None,
+ None,
+ MonthOutOfBoundsError,
+ None,
+ partial(fractional_range_check, MICROSECONDS_PER_MONTH),
+ )
+ DURATION_PNW_LIMIT = Limit(
+ "Invalid week duration string.",
+ None,
+ None,
+ WeekOutOfBoundsError,
+ None,
+ partial(fractional_range_check, MICROSECONDS_PER_WEEK),
+ )
+ DURATION_PND_LIMIT = Limit(
+ "Invalid day duration string.",
+ None,
+ None,
+ DayOutOfBoundsError,
+ None,
+ partial(fractional_range_check, MICROSECONDS_PER_DAY),
+ )
+ DURATION_TNH_LIMIT = Limit(
+ "Invalid hour duration string.",
+ None,
+ None,
+ HoursOutOfBoundsError,
+ None,
+ partial(fractional_range_check, MICROSECONDS_PER_HOUR),
+ )
+ DURATION_TNM_LIMIT = Limit(
+ "Invalid minute duration string.",
+ None,
+ None,
+ MinutesOutOfBoundsError,
+ None,
+ partial(fractional_range_check, MICROSECONDS_PER_MINUTE),
+ )
+ DURATION_TNS_LIMIT = Limit(
+ "Invalid second duration string.",
+ None,
+ None,
+ SecondsOutOfBoundsError,
+ None,
+ partial(fractional_range_check, MICROSECONDS_PER_SECOND),
+ )
+
+ DATE_RANGE_DICT = BaseTimeBuilder.DATE_RANGE_DICT
+ DATE_RANGE_DICT["YYYY"] = DATE_YYYY_LIMIT
+
+ TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT}
+
+ DURATION_RANGE_DICT = {
+ "PnY": DURATION_PNY_LIMIT,
+ "PnM": DURATION_PNM_LIMIT,
+ "PnW": DURATION_PNW_LIMIT,
+ "PnD": DURATION_PND_LIMIT,
+ "TnH": DURATION_TNH_LIMIT,
+ "TnM": DURATION_TNM_LIMIT,
+ "TnS": DURATION_TNS_LIMIT,
+ }
+
+ @classmethod
+ def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None):
+ YYYY, MM, DD, Www, D, DDD = cls.range_check_date(YYYY, MM, DD, Www, D, DDD)
+
+ if MM is None:
+ MM = 1
+
+ if DD is None:
+ DD = 1
+
+ if DDD is not None:
+ return PythonTimeBuilder._build_ordinal_date(YYYY, DDD)
+
+ if Www is not None:
+ return PythonTimeBuilder._build_week_date(YYYY, Www, isoday=D)
+
+ return datetime.date(YYYY, MM, DD)
+
+ @classmethod
+ def build_time(cls, hh=None, mm=None, ss=None, tz=None):
+ # Builds a time from the given parts, handling fractional arguments
+ # where necessary
+ hours = 0
+ minutes = 0
+ seconds = 0
+ microseconds = 0
+
+ hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz)
+
+ if type(hh) is FractionalComponent:
+ hours = hh.principal
+ microseconds = hh.microsecondremainder
+ elif hh is not None:
+ hours = hh
+
+ if type(mm) is FractionalComponent:
+ minutes = mm.principal
+ microseconds = mm.microsecondremainder
+ elif mm is not None:
+ minutes = mm
+
+ if type(ss) is FractionalComponent:
+ seconds = ss.principal
+ microseconds = ss.microsecondremainder
+ elif ss is not None:
+ seconds = ss
+
+ (
+ hours,
+ minutes,
+ seconds,
+ microseconds,
+ ) = PythonTimeBuilder._distribute_microseconds(
+ microseconds,
+ (hours, minutes, seconds),
+ (MICROSECONDS_PER_HOUR, MICROSECONDS_PER_MINUTE, MICROSECONDS_PER_SECOND),
+ )
+
+ # Move midnight into range
+ if hours == 24:
+ hours = 0
+
+ # Datetimes don't handle fractional components, so we use a timedelta
+ if tz is not None:
+ return (
+ datetime.datetime(
+ 1, 1, 1, hour=hours, minute=minutes, tzinfo=cls._build_object(tz)
+ )
+ + datetime.timedelta(seconds=seconds, microseconds=microseconds)
+ ).timetz()
+
+ return (
+ datetime.datetime(1, 1, 1, hour=hours, minute=minutes)
+ + datetime.timedelta(seconds=seconds, microseconds=microseconds)
+ ).time()
+
+ @classmethod
+ def build_datetime(cls, date, time):
+ return datetime.datetime.combine(
+ cls._build_object(date), cls._build_object(time)
+ )
+
+ @classmethod
+ def build_duration(
+ cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
+ ):
+ # PnY and PnM will be distributed to PnD, microsecond remainder to TnS
+ PnY, PnM, PnW, PnD, TnH, TnM, TnS = cls.range_check_duration(
+ PnY, PnM, PnW, PnD, TnH, TnM, TnS
+ )
+
+ seconds = TnS.principal
+ microseconds = TnS.microsecondremainder
+
+ return datetime.timedelta(
+ days=PnD,
+ seconds=seconds,
+ microseconds=microseconds,
+ minutes=TnM,
+ hours=TnH,
+ weeks=PnW,
+ )
+
+ @classmethod
+ def build_interval(cls, start=None, end=None, duration=None):
+ start, end, duration = cls.range_check_interval(start, end, duration)
+
+ if start is not None and end is not None:
+ # <start>/<end>
+ startobject = cls._build_object(start)
+ endobject = cls._build_object(end)
+
+ return (startobject, endobject)
+
+ durationobject = cls._build_object(duration)
+
+ # Determine if datetime promotion is required
+ datetimerequired = (
+ duration.TnH is not None
+ or duration.TnM is not None
+ or duration.TnS is not None
+ or durationobject.seconds != 0
+ or durationobject.microseconds != 0
+ )
+
+ if end is not None:
+ # <duration>/<end>
+ endobject = cls._build_object(end)
+
+ # Range check
+ if type(end) is DateTuple and datetimerequired is True:
+ # <end> is a date, and <duration> requires datetime resolution
+ return (
+ endobject,
+ cls.build_datetime(end, TupleBuilder.build_time()) - durationobject,
+ )
+
+ return (endobject, endobject - durationobject)
+
+ # <start>/<duration>
+ startobject = cls._build_object(start)
+
+ # Range check
+ if type(start) is DateTuple and datetimerequired is True:
+ # <start> is a date, and <duration> requires datetime resolution
+ return (
+ startobject,
+ cls.build_datetime(start, TupleBuilder.build_time()) + durationobject,
+ )
+
+ return (startobject, startobject + durationobject)
+
+ @classmethod
+ def build_repeating_interval(cls, R=None, Rnn=None, interval=None):
+ startobject = None
+ endobject = None
+
+ R, Rnn, interval = cls.range_check_repeating_interval(R, Rnn, interval)
+
+ if interval.start is not None:
+ startobject = cls._build_object(interval.start)
+
+ if interval.end is not None:
+ endobject = cls._build_object(interval.end)
+
+ if interval.duration is not None:
+ durationobject = cls._build_object(interval.duration)
+ else:
+ durationobject = endobject - startobject
+
+ if R is True:
+ if startobject is not None:
+ return cls._date_generator_unbounded(startobject, durationobject)
+
+ return cls._date_generator_unbounded(endobject, -durationobject)
+
+ iterations = int(Rnn)
+
+ if startobject is not None:
+ return cls._date_generator(startobject, durationobject, iterations)
+
+ return cls._date_generator(endobject, -durationobject, iterations)
+
+ @classmethod
+ def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""):
+ negative, Z, hh, mm, name = cls.range_check_timezone(negative, Z, hh, mm, name)
+
+ if Z is True:
+ # Z -> UTC
+ return UTCOffset(name="UTC", minutes=0)
+
+ tzhour = int(hh)
+
+ if mm is not None:
+ tzminute = int(mm)
+ else:
+ tzminute = 0
+
+ if negative is True:
+ return UTCOffset(name=name, minutes=-(tzhour * 60 + tzminute))
+
+ return UTCOffset(name=name, minutes=tzhour * 60 + tzminute)
+
+ @classmethod
+ def range_check_duration(
+ cls,
+ PnY=None,
+ PnM=None,
+ PnW=None,
+ PnD=None,
+ TnH=None,
+ TnM=None,
+ TnS=None,
+ rangedict=None,
+ ):
+ years = 0
+ months = 0
+ days = 0
+ weeks = 0
+ hours = 0
+ minutes = 0
+ seconds = 0
+ microseconds = 0
+
+ PnY, PnM, PnW, PnD, TnH, TnM, TnS = BaseTimeBuilder.range_check_duration(
+ PnY, PnM, PnW, PnD, TnH, TnM, TnS, rangedict=cls.DURATION_RANGE_DICT
+ )
+
+ if PnY is not None:
+ if type(PnY) is FractionalComponent:
+ years = PnY.principal
+ microseconds = PnY.microsecondremainder
+ else:
+ years = PnY
+
+ if years * DAYS_PER_YEAR > TIMEDELTA_MAX_DAYS:
+ raise YearOutOfBoundsError("Duration exceeds maximum timedelta size.")
+
+ if PnM is not None:
+ if type(PnM) is FractionalComponent:
+ months = PnM.principal
+ microseconds = PnM.microsecondremainder
+ else:
+ months = PnM
+
+ if months * DAYS_PER_MONTH > TIMEDELTA_MAX_DAYS:
+ raise MonthOutOfBoundsError("Duration exceeds maximum timedelta size.")
+
+ if PnW is not None:
+ if type(PnW) is FractionalComponent:
+ weeks = PnW.principal
+ microseconds = PnW.microsecondremainder
+ else:
+ weeks = PnW
+
+ if weeks * DAYS_PER_WEEK > TIMEDELTA_MAX_DAYS:
+ raise WeekOutOfBoundsError("Duration exceeds maximum timedelta size.")
+
+ if PnD is not None:
+ if type(PnD) is FractionalComponent:
+ days = PnD.principal
+ microseconds = PnD.microsecondremainder
+ else:
+ days = PnD
+
+ if days > TIMEDELTA_MAX_DAYS:
+ raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.")
+
+ if TnH is not None:
+ if type(TnH) is FractionalComponent:
+ hours = TnH.principal
+ microseconds = TnH.microsecondremainder
+ else:
+ hours = TnH
+
+ if hours // HOURS_PER_DAY > TIMEDELTA_MAX_DAYS:
+ raise HoursOutOfBoundsError("Duration exceeds maximum timedelta size.")
+
+ if TnM is not None:
+ if type(TnM) is FractionalComponent:
+ minutes = TnM.principal
+ microseconds = TnM.microsecondremainder
+ else:
+ minutes = TnM
+
+ if minutes // MINUTES_PER_DAY > TIMEDELTA_MAX_DAYS:
+ raise MinutesOutOfBoundsError(
+ "Duration exceeds maximum timedelta size."
+ )
+
+ if TnS is not None:
+ if type(TnS) is FractionalComponent:
+ seconds = TnS.principal
+ microseconds = TnS.microsecondremainder
+ else:
+ seconds = TnS
+
+ if seconds // SECONDS_PER_DAY > TIMEDELTA_MAX_DAYS:
+ raise SecondsOutOfBoundsError(
+ "Duration exceeds maximum timedelta size."
+ )
+
+ (
+ years,
+ months,
+ weeks,
+ days,
+ hours,
+ minutes,
+ seconds,
+ microseconds,
+ ) = PythonTimeBuilder._distribute_microseconds(
+ microseconds,
+ (years, months, weeks, days, hours, minutes, seconds),
+ (
+ MICROSECONDS_PER_YEAR,
+ MICROSECONDS_PER_MONTH,
+ MICROSECONDS_PER_WEEK,
+ MICROSECONDS_PER_DAY,
+ MICROSECONDS_PER_HOUR,
+ MICROSECONDS_PER_MINUTE,
+ MICROSECONDS_PER_SECOND,
+ ),
+ )
+
+ # Note that weeks can be handled without conversion to days
+ totaldays = years * DAYS_PER_YEAR + months * DAYS_PER_MONTH + days
+
+ # Check against timedelta limits
+ if (
+ totaldays
+ + weeks * DAYS_PER_WEEK
+ + hours // HOURS_PER_DAY
+ + minutes // MINUTES_PER_DAY
+ + seconds // SECONDS_PER_DAY
+ > TIMEDELTA_MAX_DAYS
+ ):
+ raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.")
+
+ return (
+ None,
+ None,
+ weeks,
+ totaldays,
+ hours,
+ minutes,
+ FractionalComponent(seconds, microseconds),
+ )
+
+ @classmethod
+ def range_check_interval(cls, start=None, end=None, duration=None):
+ # Handles concise format, range checks any potential durations
+ if start is not None and end is not None:
+ # <start>/<end>
+ # Handle concise format
+ if cls._is_interval_end_concise(end) is True:
+ end = cls._combine_concise_interval_tuples(start, end)
+
+ return (start, end, duration)
+
+ durationobject = cls._build_object(duration)
+
+ if end is not None:
+ # <duration>/<end>
+ endobject = cls._build_object(end)
+
+ # Range check
+ if type(end) is DateTuple:
+ enddatetime = cls.build_datetime(end, TupleBuilder.build_time())
+
+ if enddatetime - datetime.datetime.min < durationobject:
+ raise YearOutOfBoundsError("Interval end less than minimium date.")
+ else:
+ mindatetime = datetime.datetime.min
+
+ if end.time.tz is not None:
+ mindatetime = mindatetime.replace(tzinfo=endobject.tzinfo)
+
+ if endobject - mindatetime < durationobject:
+ raise YearOutOfBoundsError("Interval end less than minimium date.")
+ else:
+ # <start>/<duration>
+ startobject = cls._build_object(start)
+
+ # Range check
+ if type(start) is DateTuple:
+ startdatetime = cls.build_datetime(start, TupleBuilder.build_time())
+
+ if datetime.datetime.max - startdatetime < durationobject:
+ raise YearOutOfBoundsError(
+ "Interval end greater than maximum date."
+ )
+ else:
+ maxdatetime = datetime.datetime.max
+
+ if start.time.tz is not None:
+ maxdatetime = maxdatetime.replace(tzinfo=startobject.tzinfo)
+
+ if maxdatetime - startobject < durationobject:
+ raise YearOutOfBoundsError(
+ "Interval end greater than maximum date."
+ )
+
+ return (start, end, duration)
+
+ @staticmethod
+ def _build_week_date(isoyear, isoweek, isoday=None):
+ if isoday is None:
+ return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta(
+ weeks=isoweek - 1
+ )
+
+ return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta(
+ weeks=isoweek - 1, days=isoday - 1
+ )
+
+ @staticmethod
+ def _build_ordinal_date(isoyear, isoday):
+ # Day of year to a date
+ # https://stackoverflow.com/questions/2427555/python-question-year-and-day-of-year-to-date
+ builtdate = datetime.date(isoyear, 1, 1) + datetime.timedelta(days=isoday - 1)
+
+ return builtdate
+
+ @staticmethod
+ def _iso_year_start(isoyear):
+ # Given an ISO year, returns the equivalent of the start of the year
+ # on the Gregorian calendar (which is used by Python)
+ # Stolen from:
+ # http://stackoverflow.com/questions/304256/whats-the-best-way-to-find-the-inverse-of-datetime-isocalendar
+
+ # Determine the location of the 4th of January, the first week of
+ # the ISO year is the week containing the 4th of January
+ # http://en.wikipedia.org/wiki/ISO_week_date
+ fourth_jan = datetime.date(isoyear, 1, 4)
+
+ # Note the conversion from ISO day (1 - 7) and Python day (0 - 6)
+ delta = datetime.timedelta(days=fourth_jan.isoweekday() - 1)
+
+ # Return the start of the year
+ return fourth_jan - delta
+
+ @staticmethod
+ def _date_generator(startdate, timedelta, iterations):
+ currentdate = startdate
+ currentiteration = 0
+
+ while currentiteration < iterations:
+ yield currentdate
+
+ # Update the values
+ currentdate += timedelta
+ currentiteration += 1
+
+ @staticmethod
+ def _date_generator_unbounded(startdate, timedelta):
+ currentdate = startdate
+
+ while True:
+ yield currentdate
+
+ # Update the value
+ currentdate += timedelta
+
+ @staticmethod
+ def _distribute_microseconds(todistribute, recipients, reductions):
+ # Given a number of microseconds as int, a tuple of ints length n
+ # to distribute to, and a tuple of ints length n to divide todistribute
+ # by (from largest to smallest), returns a tuple of length n + 1, with
+ # todistribute divided across recipients using the reductions, with
+ # the final remainder returned as the final tuple member
+ results = []
+
+ remainder = todistribute
+
+ for index, reduction in enumerate(reductions):
+ additional, remainder = divmod(remainder, reduction)
+
+ results.append(recipients[index] + additional)
+
+ # Always return the remaining microseconds
+ results.append(remainder)
+
+ return tuple(results)
diff --git a/libs/aniso8601/builders/tests/__init__.py b/libs/aniso8601/builders/tests/__init__.py
new file mode 100644
index 000000000..1a94e017a
--- /dev/null
+++ b/libs/aniso8601/builders/tests/__init__.py
@@ -0,0 +1,7 @@
+# -*- 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.
diff --git a/libs/aniso8601/builders/tests/test_init.py b/libs/aniso8601/builders/tests/test_init.py
new file mode 100644
index 000000000..7c9f092c7
--- /dev/null
+++ b/libs/aniso8601/builders/tests/test_init.py
@@ -0,0 +1,838 @@
+# -*- 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.
+
+import unittest
+
+import aniso8601
+from aniso8601.builders import (
+ BaseTimeBuilder,
+ DatetimeTuple,
+ DateTuple,
+ DurationTuple,
+ IntervalTuple,
+ RepeatingIntervalTuple,
+ TimeTuple,
+ TimezoneTuple,
+ TupleBuilder,
+ cast,
+)
+from aniso8601.exceptions import (
+ DayOutOfBoundsError,
+ HoursOutOfBoundsError,
+ ISOFormatError,
+ LeapSecondError,
+ MidnightBoundsError,
+ MinutesOutOfBoundsError,
+ MonthOutOfBoundsError,
+ SecondsOutOfBoundsError,
+ WeekOutOfBoundsError,
+)
+from aniso8601.tests.compat import mock
+
+
+class LeapSecondSupportingTestBuilder(BaseTimeBuilder):
+ LEAP_SECONDS_SUPPORTED = True
+
+
+class TestBuilderFunctions(unittest.TestCase):
+ def test_cast(self):
+ self.assertEqual(cast("1", int), 1)
+ self.assertEqual(cast("-2", int), -2)
+ self.assertEqual(cast("3", float), float(3))
+ self.assertEqual(cast("-4", float), float(-4))
+ self.assertEqual(cast("5.6", float), 5.6)
+ self.assertEqual(cast("-7.8", float), -7.8)
+
+ def test_cast_exception(self):
+ with self.assertRaises(ISOFormatError):
+ cast("asdf", int)
+
+ with self.assertRaises(ISOFormatError):
+ cast("asdf", float)
+
+ def test_cast_caughtexception(self):
+ def tester(value):
+ raise RuntimeError
+
+ with self.assertRaises(ISOFormatError):
+ cast("asdf", tester, caughtexceptions=(RuntimeError,))
+
+ def test_cast_thrownexception(self):
+ with self.assertRaises(RuntimeError):
+ cast("asdf", int, thrownexception=RuntimeError)
+
+
+class TestBaseTimeBuilder(unittest.TestCase):
+ def test_build_date(self):
+ with self.assertRaises(NotImplementedError):
+ BaseTimeBuilder.build_date()
+
+ def test_build_time(self):
+ with self.assertRaises(NotImplementedError):
+ BaseTimeBuilder.build_time()
+
+ def test_build_datetime(self):
+ with self.assertRaises(NotImplementedError):
+ BaseTimeBuilder.build_datetime(None, None)
+
+ def test_build_duration(self):
+ with self.assertRaises(NotImplementedError):
+ BaseTimeBuilder.build_duration()
+
+ def test_build_interval(self):
+ with self.assertRaises(NotImplementedError):
+ BaseTimeBuilder.build_interval()
+
+ def test_build_repeating_interval(self):
+ with self.assertRaises(NotImplementedError):
+ BaseTimeBuilder.build_repeating_interval()
+
+ def test_build_timezone(self):
+ with self.assertRaises(NotImplementedError):
+ BaseTimeBuilder.build_timezone()
+
+ def test_range_check_date(self):
+ # Check the calendar for day ranges
+ with self.assertRaises(DayOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="0007", MM="02", DD="30")
+
+ with self.assertRaises(DayOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="0007", DDD="366")
+
+ with self.assertRaises(MonthOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="4333", MM="30", DD="30")
+
+ # 0 isn't a valid week number
+ with self.assertRaises(WeekOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="2003", Www="00")
+
+ # Week must not be larger than 53
+ with self.assertRaises(WeekOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="2004", Www="54")
+
+ # 0 isn't a valid day number
+ with self.assertRaises(DayOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="2001", Www="02", D="0")
+
+ # Day must not be larger than 7
+ with self.assertRaises(DayOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="2001", Www="02", D="8")
+
+ with self.assertRaises(DayOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="1981", DDD="000")
+
+ # Day must be 365, or 366, not larger
+ with self.assertRaises(DayOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="1234", DDD="000")
+
+ with self.assertRaises(DayOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="1234", DDD="367")
+
+ # https://bitbucket.org/nielsenb/aniso8601/issues/14/parsing-ordinal-dates-should-only-allow
+ with self.assertRaises(DayOutOfBoundsError):
+ BaseTimeBuilder.range_check_date(YYYY="1981", DDD="366")
+
+ # Make sure Nones pass through unmodified
+ self.assertEqual(
+ BaseTimeBuilder.range_check_date(rangedict={}),
+ (None, None, None, None, None, None),
+ )
+
+ def test_range_check_time(self):
+ # Leap seconds not supported
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ # https://bitbucket.org/nielsenb/aniso8601/issues/13/parsing-of-leap-second-gives-wildly
+ with self.assertRaises(LeapSecondError):
+ BaseTimeBuilder.range_check_time(hh="23", mm="59", ss="60")
+
+ with self.assertRaises(SecondsOutOfBoundsError):
+ BaseTimeBuilder.range_check_time(hh="00", mm="00", ss="60")
+
+ with self.assertRaises(SecondsOutOfBoundsError):
+ BaseTimeBuilder.range_check_time(hh="00", mm="00", ss="61")
+
+ with self.assertRaises(MinutesOutOfBoundsError):
+ BaseTimeBuilder.range_check_time(hh="00", mm="61")
+
+ with self.assertRaises(MinutesOutOfBoundsError):
+ BaseTimeBuilder.range_check_time(hh="00", mm="60")
+
+ with self.assertRaises(MinutesOutOfBoundsError):
+ BaseTimeBuilder.range_check_time(hh="00", mm="60.1")
+
+ with self.assertRaises(HoursOutOfBoundsError):
+ BaseTimeBuilder.range_check_time(hh="25")
+
+ # Hour 24 can only represent midnight
+ with self.assertRaises(MidnightBoundsError):
+ BaseTimeBuilder.range_check_time(hh="24", mm="00", ss="01")
+
+ with self.assertRaises(MidnightBoundsError):
+ BaseTimeBuilder.range_check_time(hh="24", mm="00.1")
+
+ with self.assertRaises(MidnightBoundsError):
+ BaseTimeBuilder.range_check_time(hh="24", mm="01")
+
+ with self.assertRaises(MidnightBoundsError):
+ BaseTimeBuilder.range_check_time(hh="24.1")
+
+ # Leap seconds not supported
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ # https://bitbucket.org/nielsenb/aniso8601/issues/13/parsing-of-leap-second-gives-wildly
+ with self.assertRaises(LeapSecondError):
+ BaseTimeBuilder.range_check_time(hh="23", mm="59", ss="60")
+
+ # Make sure Nones pass through unmodified
+ self.assertEqual(
+ BaseTimeBuilder.range_check_time(rangedict={}), (None, None, None, None)
+ )
+
+ def test_range_check_time_leap_seconds_supported(self):
+ self.assertEqual(
+ LeapSecondSupportingTestBuilder.range_check_time(hh="23", mm="59", ss="60"),
+ (23, 59, 60, None),
+ )
+
+ with self.assertRaises(SecondsOutOfBoundsError):
+ LeapSecondSupportingTestBuilder.range_check_time(hh="01", mm="02", ss="60")
+
+ def test_range_check_duration(self):
+ self.assertEqual(
+ BaseTimeBuilder.range_check_duration(),
+ (None, None, None, None, None, None, None),
+ )
+
+ self.assertEqual(
+ BaseTimeBuilder.range_check_duration(rangedict={}),
+ (None, None, None, None, None, None, None),
+ )
+
+ def test_range_check_repeating_interval(self):
+ self.assertEqual(
+ BaseTimeBuilder.range_check_repeating_interval(), (None, None, None)
+ )
+
+ self.assertEqual(
+ BaseTimeBuilder.range_check_repeating_interval(rangedict={}),
+ (None, None, None),
+ )
+
+ def test_range_check_timezone(self):
+ self.assertEqual(
+ BaseTimeBuilder.range_check_timezone(), (None, None, None, None, "")
+ )
+
+ self.assertEqual(
+ BaseTimeBuilder.range_check_timezone(rangedict={}),
+ (None, None, None, None, ""),
+ )
+
+ def test_build_object(self):
+ datetest = (
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ {"YYYY": "1", "MM": "2", "DD": "3", "Www": "4", "D": "5", "DDD": "6"},
+ )
+
+ timetest = (
+ TimeTuple("1", "2", "3", TimezoneTuple(False, False, "4", "5", "tz name")),
+ {
+ "hh": "1",
+ "mm": "2",
+ "ss": "3",
+ "tz": TimezoneTuple(False, False, "4", "5", "tz name"),
+ },
+ )
+
+ datetimetest = (
+ DatetimeTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ TimeTuple(
+ "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name")
+ ),
+ ),
+ (
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ TimeTuple(
+ "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name")
+ ),
+ ),
+ )
+
+ durationtest = (
+ DurationTuple("1", "2", "3", "4", "5", "6", "7"),
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": "3",
+ "PnD": "4",
+ "TnH": "5",
+ "TnM": "6",
+ "TnS": "7",
+ },
+ )
+
+ intervaltests = (
+ (
+ IntervalTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ DateTuple("7", "8", "9", "10", "11", "12"),
+ None,
+ ),
+ {
+ "start": DateTuple("1", "2", "3", "4", "5", "6"),
+ "end": DateTuple("7", "8", "9", "10", "11", "12"),
+ "duration": None,
+ },
+ ),
+ (
+ IntervalTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ None,
+ DurationTuple("7", "8", "9", "10", "11", "12", "13"),
+ ),
+ {
+ "start": DateTuple("1", "2", "3", "4", "5", "6"),
+ "end": None,
+ "duration": DurationTuple("7", "8", "9", "10", "11", "12", "13"),
+ },
+ ),
+ (
+ IntervalTuple(
+ None,
+ TimeTuple(
+ "1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name")
+ ),
+ DurationTuple("6", "7", "8", "9", "10", "11", "12"),
+ ),
+ {
+ "start": None,
+ "end": TimeTuple(
+ "1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name")
+ ),
+ "duration": DurationTuple("6", "7", "8", "9", "10", "11", "12"),
+ },
+ ),
+ )
+
+ repeatingintervaltests = (
+ (
+ RepeatingIntervalTuple(
+ True,
+ None,
+ IntervalTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ DateTuple("7", "8", "9", "10", "11", "12"),
+ None,
+ ),
+ ),
+ {
+ "R": True,
+ "Rnn": None,
+ "interval": IntervalTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ DateTuple("7", "8", "9", "10", "11", "12"),
+ None,
+ ),
+ },
+ ),
+ (
+ RepeatingIntervalTuple(
+ False,
+ "1",
+ IntervalTuple(
+ DatetimeTuple(
+ DateTuple("2", "3", "4", "5", "6", "7"),
+ TimeTuple("8", "9", "10", None),
+ ),
+ DatetimeTuple(
+ DateTuple("11", "12", "13", "14", "15", "16"),
+ TimeTuple("17", "18", "19", None),
+ ),
+ None,
+ ),
+ ),
+ {
+ "R": False,
+ "Rnn": "1",
+ "interval": IntervalTuple(
+ DatetimeTuple(
+ DateTuple("2", "3", "4", "5", "6", "7"),
+ TimeTuple("8", "9", "10", None),
+ ),
+ DatetimeTuple(
+ DateTuple("11", "12", "13", "14", "15", "16"),
+ TimeTuple("17", "18", "19", None),
+ ),
+ None,
+ ),
+ },
+ ),
+ )
+
+ timezonetest = (
+ TimezoneTuple(False, False, "1", "2", "+01:02"),
+ {"negative": False, "Z": False, "hh": "1", "mm": "2", "name": "+01:02"},
+ )
+
+ with mock.patch.object(
+ aniso8601.builders.BaseTimeBuilder, "build_date"
+ ) as mock_build:
+ mock_build.return_value = datetest[0]
+
+ result = BaseTimeBuilder._build_object(datetest[0])
+
+ self.assertEqual(result, datetest[0])
+ mock_build.assert_called_once_with(**datetest[1])
+
+ with mock.patch.object(
+ aniso8601.builders.BaseTimeBuilder, "build_time"
+ ) as mock_build:
+ mock_build.return_value = timetest[0]
+
+ result = BaseTimeBuilder._build_object(timetest[0])
+
+ self.assertEqual(result, timetest[0])
+ mock_build.assert_called_once_with(**timetest[1])
+
+ with mock.patch.object(
+ aniso8601.builders.BaseTimeBuilder, "build_datetime"
+ ) as mock_build:
+ mock_build.return_value = datetimetest[0]
+
+ result = BaseTimeBuilder._build_object(datetimetest[0])
+
+ self.assertEqual(result, datetimetest[0])
+ mock_build.assert_called_once_with(*datetimetest[1])
+
+ with mock.patch.object(
+ aniso8601.builders.BaseTimeBuilder, "build_duration"
+ ) as mock_build:
+ mock_build.return_value = durationtest[0]
+
+ result = BaseTimeBuilder._build_object(durationtest[0])
+
+ self.assertEqual(result, durationtest[0])
+ mock_build.assert_called_once_with(**durationtest[1])
+
+ for intervaltest in intervaltests:
+ with mock.patch.object(
+ aniso8601.builders.BaseTimeBuilder, "build_interval"
+ ) as mock_build:
+ mock_build.return_value = intervaltest[0]
+
+ result = BaseTimeBuilder._build_object(intervaltest[0])
+
+ self.assertEqual(result, intervaltest[0])
+ mock_build.assert_called_once_with(**intervaltest[1])
+
+ for repeatingintervaltest in repeatingintervaltests:
+ with mock.patch.object(
+ aniso8601.builders.BaseTimeBuilder, "build_repeating_interval"
+ ) as mock_build:
+ mock_build.return_value = repeatingintervaltest[0]
+
+ result = BaseTimeBuilder._build_object(repeatingintervaltest[0])
+
+ self.assertEqual(result, repeatingintervaltest[0])
+ mock_build.assert_called_once_with(**repeatingintervaltest[1])
+
+ with mock.patch.object(
+ aniso8601.builders.BaseTimeBuilder, "build_timezone"
+ ) as mock_build:
+ mock_build.return_value = timezonetest[0]
+
+ result = BaseTimeBuilder._build_object(timezonetest[0])
+
+ self.assertEqual(result, timezonetest[0])
+ mock_build.assert_called_once_with(**timezonetest[1])
+
+ def test_is_interval_end_concise(self):
+ self.assertTrue(
+ BaseTimeBuilder._is_interval_end_concise(TimeTuple("1", "2", "3", None))
+ )
+ self.assertTrue(
+ BaseTimeBuilder._is_interval_end_concise(
+ DateTuple(None, "2", "3", "4", "5", "6")
+ )
+ )
+ self.assertTrue(
+ BaseTimeBuilder._is_interval_end_concise(
+ DatetimeTuple(
+ DateTuple(None, "2", "3", "4", "5", "6"),
+ TimeTuple("7", "8", "9", None),
+ )
+ )
+ )
+
+ self.assertFalse(
+ BaseTimeBuilder._is_interval_end_concise(
+ DateTuple("1", "2", "3", "4", "5", "6")
+ )
+ )
+ self.assertFalse(
+ BaseTimeBuilder._is_interval_end_concise(
+ DatetimeTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ TimeTuple("7", "8", "9", None),
+ )
+ )
+ )
+
+ def test_combine_concise_interval_tuples(self):
+ testtuples = (
+ (
+ DateTuple("2020", "01", "01", None, None, None),
+ DateTuple(None, None, "02", None, None, None),
+ DateTuple("2020", "01", "02", None, None, None),
+ ),
+ (
+ DateTuple("2008", "02", "15", None, None, None),
+ DateTuple(None, "03", "14", None, None, None),
+ DateTuple("2008", "03", "14", None, None, None),
+ ),
+ (
+ DatetimeTuple(
+ DateTuple("2007", "12", "14", None, None, None),
+ TimeTuple("13", "30", None, None),
+ ),
+ TimeTuple("15", "30", None, None),
+ DatetimeTuple(
+ DateTuple("2007", "12", "14", None, None, None),
+ TimeTuple("15", "30", None, None),
+ ),
+ ),
+ (
+ DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("09", "00", None, None),
+ ),
+ DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ DatetimeTuple(
+ DateTuple("2007", "11", "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ ),
+ (
+ DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ DatetimeTuple(
+ DateTuple(None, None, "16", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ DatetimeTuple(
+ DateTuple("2007", "11", "16", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ ),
+ (
+ DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple(
+ "09", "00", None, TimezoneTuple(False, True, None, None, "Z")
+ ),
+ ),
+ DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ DatetimeTuple(
+ DateTuple("2007", "11", "15", None, None, None),
+ TimeTuple(
+ "17", "00", None, TimezoneTuple(False, True, None, None, "Z")
+ ),
+ ),
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = BaseTimeBuilder._combine_concise_interval_tuples(
+ testtuple[0], testtuple[1]
+ )
+ self.assertEqual(result, testtuple[2])
+
+
+class TestTupleBuilder(unittest.TestCase):
+ def test_build_date(self):
+ datetuple = TupleBuilder.build_date()
+
+ self.assertEqual(datetuple, DateTuple(None, None, None, None, None, None))
+
+ datetuple = TupleBuilder.build_date(
+ YYYY="1", MM="2", DD="3", Www="4", D="5", DDD="6"
+ )
+
+ self.assertEqual(datetuple, DateTuple("1", "2", "3", "4", "5", "6"))
+
+ def test_build_time(self):
+ testtuples = (
+ ({}, TimeTuple(None, None, None, None)),
+ (
+ {"hh": "1", "mm": "2", "ss": "3", "tz": None},
+ TimeTuple("1", "2", "3", None),
+ ),
+ (
+ {
+ "hh": "1",
+ "mm": "2",
+ "ss": "3",
+ "tz": TimezoneTuple(False, False, "4", "5", "tz name"),
+ },
+ TimeTuple(
+ "1", "2", "3", TimezoneTuple(False, False, "4", "5", "tz name")
+ ),
+ ),
+ )
+
+ for testtuple in testtuples:
+ self.assertEqual(TupleBuilder.build_time(**testtuple[0]), testtuple[1])
+
+ def test_build_datetime(self):
+ testtuples = (
+ (
+ {
+ "date": DateTuple("1", "2", "3", "4", "5", "6"),
+ "time": TimeTuple("7", "8", "9", None),
+ },
+ DatetimeTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ TimeTuple("7", "8", "9", None),
+ ),
+ ),
+ (
+ {
+ "date": DateTuple("1", "2", "3", "4", "5", "6"),
+ "time": TimeTuple(
+ "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name")
+ ),
+ },
+ DatetimeTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ TimeTuple(
+ "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name")
+ ),
+ ),
+ ),
+ )
+
+ for testtuple in testtuples:
+ self.assertEqual(TupleBuilder.build_datetime(**testtuple[0]), testtuple[1])
+
+ def test_build_duration(self):
+ testtuples = (
+ ({}, DurationTuple(None, None, None, None, None, None, None)),
+ (
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": "3",
+ "PnD": "4",
+ "TnH": "5",
+ "TnM": "6",
+ "TnS": "7",
+ },
+ DurationTuple("1", "2", "3", "4", "5", "6", "7"),
+ ),
+ )
+
+ for testtuple in testtuples:
+ self.assertEqual(TupleBuilder.build_duration(**testtuple[0]), testtuple[1])
+
+ def test_build_interval(self):
+ testtuples = (
+ ({}, IntervalTuple(None, None, None)),
+ (
+ {
+ "start": DateTuple("1", "2", "3", "4", "5", "6"),
+ "end": DateTuple("7", "8", "9", "10", "11", "12"),
+ },
+ IntervalTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ DateTuple("7", "8", "9", "10", "11", "12"),
+ None,
+ ),
+ ),
+ (
+ {
+ "start": TimeTuple(
+ "1", "2", "3", TimezoneTuple(True, False, "7", "8", "tz name")
+ ),
+ "end": TimeTuple(
+ "4", "5", "6", TimezoneTuple(False, False, "9", "10", "tz name")
+ ),
+ },
+ IntervalTuple(
+ TimeTuple(
+ "1", "2", "3", TimezoneTuple(True, False, "7", "8", "tz name")
+ ),
+ TimeTuple(
+ "4", "5", "6", TimezoneTuple(False, False, "9", "10", "tz name")
+ ),
+ None,
+ ),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ TimeTuple(
+ "7",
+ "8",
+ "9",
+ TimezoneTuple(True, False, "10", "11", "tz name"),
+ ),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("12", "13", "14", "15", "16", "17"),
+ TimeTuple(
+ "18",
+ "19",
+ "20",
+ TimezoneTuple(False, False, "21", "22", "tz name"),
+ ),
+ ),
+ },
+ IntervalTuple(
+ DatetimeTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ TimeTuple(
+ "7",
+ "8",
+ "9",
+ TimezoneTuple(True, False, "10", "11", "tz name"),
+ ),
+ ),
+ DatetimeTuple(
+ DateTuple("12", "13", "14", "15", "16", "17"),
+ TimeTuple(
+ "18",
+ "19",
+ "20",
+ TimezoneTuple(False, False, "21", "22", "tz name"),
+ ),
+ ),
+ None,
+ ),
+ ),
+ (
+ {
+ "start": DateTuple("1", "2", "3", "4", "5", "6"),
+ "end": None,
+ "duration": DurationTuple("7", "8", "9", "10", "11", "12", "13"),
+ },
+ IntervalTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ None,
+ DurationTuple("7", "8", "9", "10", "11", "12", "13"),
+ ),
+ ),
+ (
+ {
+ "start": None,
+ "end": TimeTuple(
+ "1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name")
+ ),
+ "duration": DurationTuple("6", "7", "8", "9", "10", "11", "12"),
+ },
+ IntervalTuple(
+ None,
+ TimeTuple(
+ "1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name")
+ ),
+ DurationTuple("6", "7", "8", "9", "10", "11", "12"),
+ ),
+ ),
+ )
+
+ for testtuple in testtuples:
+ self.assertEqual(TupleBuilder.build_interval(**testtuple[0]), testtuple[1])
+
+ def test_build_repeating_interval(self):
+ testtuples = (
+ ({}, RepeatingIntervalTuple(None, None, None)),
+ (
+ {
+ "R": True,
+ "interval": IntervalTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ DateTuple("7", "8", "9", "10", "11", "12"),
+ None,
+ ),
+ },
+ RepeatingIntervalTuple(
+ True,
+ None,
+ IntervalTuple(
+ DateTuple("1", "2", "3", "4", "5", "6"),
+ DateTuple("7", "8", "9", "10", "11", "12"),
+ None,
+ ),
+ ),
+ ),
+ (
+ {
+ "R": False,
+ "Rnn": "1",
+ "interval": IntervalTuple(
+ DatetimeTuple(
+ DateTuple("2", "3", "4", "5", "6", "7"),
+ TimeTuple("8", "9", "10", None),
+ ),
+ DatetimeTuple(
+ DateTuple("11", "12", "13", "14", "15", "16"),
+ TimeTuple("17", "18", "19", None),
+ ),
+ None,
+ ),
+ },
+ RepeatingIntervalTuple(
+ False,
+ "1",
+ IntervalTuple(
+ DatetimeTuple(
+ DateTuple("2", "3", "4", "5", "6", "7"),
+ TimeTuple("8", "9", "10", None),
+ ),
+ DatetimeTuple(
+ DateTuple("11", "12", "13", "14", "15", "16"),
+ TimeTuple("17", "18", "19", None),
+ ),
+ None,
+ ),
+ ),
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = TupleBuilder.build_repeating_interval(**testtuple[0])
+ self.assertEqual(result, testtuple[1])
+
+ def test_build_timezone(self):
+ testtuples = (
+ ({}, TimezoneTuple(None, None, None, None, "")),
+ (
+ {"negative": False, "Z": True, "name": "UTC"},
+ TimezoneTuple(False, True, None, None, "UTC"),
+ ),
+ (
+ {"negative": False, "Z": False, "hh": "1", "mm": "2", "name": "+01:02"},
+ TimezoneTuple(False, False, "1", "2", "+01:02"),
+ ),
+ (
+ {"negative": True, "Z": False, "hh": "1", "mm": "2", "name": "-01:02"},
+ TimezoneTuple(True, False, "1", "2", "-01:02"),
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = TupleBuilder.build_timezone(**testtuple[0])
+ self.assertEqual(result, testtuple[1])
diff --git a/libs/aniso8601/builders/tests/test_python.py b/libs/aniso8601/builders/tests/test_python.py
new file mode 100644
index 000000000..11111a163
--- /dev/null
+++ b/libs/aniso8601/builders/tests/test_python.py
@@ -0,0 +1,1710 @@
+# -*- 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.
+
+import datetime
+import unittest
+
+from aniso8601 import compat
+from aniso8601.builders import (
+ DatetimeTuple,
+ DateTuple,
+ DurationTuple,
+ IntervalTuple,
+ Limit,
+ TimeTuple,
+ TimezoneTuple,
+)
+from aniso8601.builders.python import (
+ FractionalComponent,
+ PythonTimeBuilder,
+ _cast_to_fractional_component,
+ fractional_range_check,
+ year_range_check,
+)
+from aniso8601.exceptions import (
+ DayOutOfBoundsError,
+ HoursOutOfBoundsError,
+ ISOFormatError,
+ LeapSecondError,
+ MidnightBoundsError,
+ MinutesOutOfBoundsError,
+ MonthOutOfBoundsError,
+ SecondsOutOfBoundsError,
+ WeekOutOfBoundsError,
+ YearOutOfBoundsError,
+)
+from aniso8601.utcoffset import UTCOffset
+
+
+class TestPythonTimeBuilder_UtiltyFunctions(unittest.TestCase):
+ def test_year_range_check(self):
+ yearlimit = Limit(
+ "Invalid year string.",
+ 0000,
+ 9999,
+ YearOutOfBoundsError,
+ "Year must be between 1..9999.",
+ None,
+ )
+
+ self.assertEqual(year_range_check("1", yearlimit), 1000)
+
+ def test_fractional_range_check(self):
+ limit = Limit(
+ "Invalid string.", -1, 1, ValueError, "Value must be between -1..1.", None
+ )
+
+ self.assertEqual(fractional_range_check(10, "1", limit), 1)
+ self.assertEqual(fractional_range_check(10, "-1", limit), -1)
+ self.assertEqual(
+ fractional_range_check(10, "0.1", limit), FractionalComponent(0, 1)
+ )
+ self.assertEqual(
+ fractional_range_check(10, "-0.1", limit), FractionalComponent(-0, 1)
+ )
+
+ with self.assertRaises(ValueError):
+ fractional_range_check(10, "1.1", limit)
+
+ with self.assertRaises(ValueError):
+ fractional_range_check(10, "-1.1", limit)
+
+ def test_cast_to_fractional_component(self):
+ self.assertEqual(
+ _cast_to_fractional_component(10, "1.1"), FractionalComponent(1, 1)
+ )
+ self.assertEqual(
+ _cast_to_fractional_component(10, "-1.1"), FractionalComponent(-1, 1)
+ )
+
+ self.assertEqual(
+ _cast_to_fractional_component(100, "1.1"), FractionalComponent(1, 10)
+ )
+ self.assertEqual(
+ _cast_to_fractional_component(100, "-1.1"), FractionalComponent(-1, 10)
+ )
+
+
+class TestPythonTimeBuilder(unittest.TestCase):
+ def test_build_date(self):
+ testtuples = (
+ (
+ {
+ "YYYY": "2013",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(2013, 1, 1),
+ ),
+ (
+ {
+ "YYYY": "0001",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(1, 1, 1),
+ ),
+ (
+ {
+ "YYYY": "1900",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(1900, 1, 1),
+ ),
+ (
+ {
+ "YYYY": "1981",
+ "MM": "04",
+ "DD": "05",
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(1981, 4, 5),
+ ),
+ (
+ {
+ "YYYY": "1981",
+ "MM": "04",
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(1981, 4, 1),
+ ),
+ (
+ {
+ "YYYY": "1981",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": "095",
+ },
+ datetime.date(1981, 4, 5),
+ ),
+ (
+ {
+ "YYYY": "1981",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": "365",
+ },
+ datetime.date(1981, 12, 31),
+ ),
+ (
+ {
+ "YYYY": "1980",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": "366",
+ },
+ datetime.date(1980, 12, 31),
+ ),
+ # Make sure we shift in zeros
+ (
+ {
+ "YYYY": "1",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(1000, 1, 1),
+ ),
+ (
+ {
+ "YYYY": "12",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(1200, 1, 1),
+ ),
+ (
+ {
+ "YYYY": "123",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(1230, 1, 1),
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = PythonTimeBuilder.build_date(**testtuple[0])
+ self.assertEqual(result, testtuple[1])
+
+ # Test weekday
+ testtuples = (
+ (
+ {
+ "YYYY": "2004",
+ "MM": None,
+ "DD": None,
+ "Www": "53",
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(2004, 12, 27),
+ 0,
+ ),
+ (
+ {
+ "YYYY": "2009",
+ "MM": None,
+ "DD": None,
+ "Www": "01",
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(2008, 12, 29),
+ 0,
+ ),
+ (
+ {
+ "YYYY": "2010",
+ "MM": None,
+ "DD": None,
+ "Www": "01",
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(2010, 1, 4),
+ 0,
+ ),
+ (
+ {
+ "YYYY": "2009",
+ "MM": None,
+ "DD": None,
+ "Www": "53",
+ "D": None,
+ "DDD": None,
+ },
+ datetime.date(2009, 12, 28),
+ 0,
+ ),
+ (
+ {
+ "YYYY": "2009",
+ "MM": None,
+ "DD": None,
+ "Www": "01",
+ "D": "1",
+ "DDD": None,
+ },
+ datetime.date(2008, 12, 29),
+ 0,
+ ),
+ (
+ {
+ "YYYY": "2009",
+ "MM": None,
+ "DD": None,
+ "Www": "53",
+ "D": "7",
+ "DDD": None,
+ },
+ datetime.date(2010, 1, 3),
+ 6,
+ ),
+ (
+ {
+ "YYYY": "2010",
+ "MM": None,
+ "DD": None,
+ "Www": "01",
+ "D": "1",
+ "DDD": None,
+ },
+ datetime.date(2010, 1, 4),
+ 0,
+ ),
+ (
+ {
+ "YYYY": "2004",
+ "MM": None,
+ "DD": None,
+ "Www": "53",
+ "D": "6",
+ "DDD": None,
+ },
+ datetime.date(2005, 1, 1),
+ 5,
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = PythonTimeBuilder.build_date(**testtuple[0])
+ self.assertEqual(result, testtuple[1])
+ self.assertEqual(result.weekday(), testtuple[2])
+
+ def test_build_time(self):
+ testtuples = (
+ ({}, datetime.time()),
+ ({"hh": "12.5"}, datetime.time(hour=12, minute=30)),
+ (
+ {"hh": "23.99999999997"},
+ datetime.time(hour=23, minute=59, second=59, microsecond=999999),
+ ),
+ ({"hh": "1", "mm": "23"}, datetime.time(hour=1, minute=23)),
+ (
+ {"hh": "1", "mm": "23.4567"},
+ datetime.time(hour=1, minute=23, second=27, microsecond=402000),
+ ),
+ (
+ {"hh": "14", "mm": "43.999999997"},
+ datetime.time(hour=14, minute=43, second=59, microsecond=999999),
+ ),
+ (
+ {"hh": "1", "mm": "23", "ss": "45"},
+ datetime.time(hour=1, minute=23, second=45),
+ ),
+ (
+ {"hh": "23", "mm": "21", "ss": "28.512400"},
+ datetime.time(hour=23, minute=21, second=28, microsecond=512400),
+ ),
+ (
+ {"hh": "01", "mm": "03", "ss": "11.858714"},
+ datetime.time(hour=1, minute=3, second=11, microsecond=858714),
+ ),
+ (
+ {"hh": "14", "mm": "43", "ss": "59.9999997"},
+ datetime.time(hour=14, minute=43, second=59, microsecond=999999),
+ ),
+ ({"hh": "24"}, datetime.time(hour=0)),
+ ({"hh": "24", "mm": "00"}, datetime.time(hour=0)),
+ ({"hh": "24", "mm": "00", "ss": "00"}, datetime.time(hour=0)),
+ (
+ {"tz": TimezoneTuple(False, None, "00", "00", "UTC")},
+ datetime.time(tzinfo=UTCOffset(name="UTC", minutes=0)),
+ ),
+ (
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ datetime.time(
+ hour=23,
+ minute=21,
+ second=28,
+ microsecond=512400,
+ tzinfo=UTCOffset(name="+00:00", minutes=0),
+ ),
+ ),
+ (
+ {
+ "hh": "1",
+ "mm": "23",
+ "tz": TimezoneTuple(False, None, "01", "00", "+1"),
+ },
+ datetime.time(
+ hour=1, minute=23, tzinfo=UTCOffset(name="+1", minutes=60)
+ ),
+ ),
+ (
+ {
+ "hh": "1",
+ "mm": "23.4567",
+ "tz": TimezoneTuple(True, None, "01", "00", "-1"),
+ },
+ datetime.time(
+ hour=1,
+ minute=23,
+ second=27,
+ microsecond=402000,
+ tzinfo=UTCOffset(name="-1", minutes=-60),
+ ),
+ ),
+ (
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "01", "30", "+1:30"),
+ },
+ datetime.time(
+ hour=23,
+ minute=21,
+ second=28,
+ microsecond=512400,
+ tzinfo=UTCOffset(name="+1:30", minutes=90),
+ ),
+ ),
+ (
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "11", "15", "+11:15"),
+ },
+ datetime.time(
+ hour=23,
+ minute=21,
+ second=28,
+ microsecond=512400,
+ tzinfo=UTCOffset(name="+11:15", minutes=675),
+ ),
+ ),
+ (
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "12", "34", "+12:34"),
+ },
+ datetime.time(
+ hour=23,
+ minute=21,
+ second=28,
+ microsecond=512400,
+ tzinfo=UTCOffset(name="+12:34", minutes=754),
+ ),
+ ),
+ (
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "00", "00", "UTC"),
+ },
+ datetime.time(
+ hour=23,
+ minute=21,
+ second=28,
+ microsecond=512400,
+ tzinfo=UTCOffset(name="UTC", minutes=0),
+ ),
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ # https://bitbucket.org/nielsenb/aniso8601/issues/21/sub-microsecond-precision-is-lost-when
+ (
+ {"hh": "14.9999999999999999"},
+ datetime.time(hour=14, minute=59, second=59, microsecond=999999),
+ ),
+ ({"mm": "0.00000000999"}, datetime.time()),
+ ({"mm": "0.0000000999"}, datetime.time(microsecond=5)),
+ ({"ss": "0.0000001"}, datetime.time()),
+ ({"ss": "2.0000048"}, datetime.time(second=2, microsecond=4)),
+ )
+
+ for testtuple in testtuples:
+ result = PythonTimeBuilder.build_time(**testtuple[0])
+ self.assertEqual(result, testtuple[1])
+
+ def test_build_datetime(self):
+ testtuples = (
+ (
+ (
+ DateTuple("2019", "06", "05", None, None, None),
+ TimeTuple("01", "03", "11.858714", None),
+ ),
+ datetime.datetime(
+ 2019, 6, 5, hour=1, minute=3, second=11, microsecond=858714
+ ),
+ ),
+ (
+ (
+ DateTuple("1234", "02", "03", None, None, None),
+ TimeTuple("23", "21", "28.512400", None),
+ ),
+ datetime.datetime(
+ 1234, 2, 3, hour=23, minute=21, second=28, microsecond=512400
+ ),
+ ),
+ (
+ (
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple(
+ "23",
+ "21",
+ "28.512400",
+ TimezoneTuple(False, None, "11", "15", "+11:15"),
+ ),
+ ),
+ datetime.datetime(
+ 1981,
+ 4,
+ 5,
+ hour=23,
+ minute=21,
+ second=28,
+ microsecond=512400,
+ tzinfo=UTCOffset(name="+11:15", minutes=675),
+ ),
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = PythonTimeBuilder.build_datetime(*testtuple[0])
+ self.assertEqual(result, testtuple[1])
+
+ def test_build_duration(self):
+ testtuples = (
+ (
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ datetime.timedelta(days=428, hours=4, minutes=54, seconds=6),
+ ),
+ (
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ datetime.timedelta(days=428, hours=4, minutes=54, seconds=6.5),
+ ),
+ ({"PnY": "1", "PnM": "2", "PnD": "3"}, datetime.timedelta(days=428)),
+ ({"PnY": "1", "PnM": "2", "PnD": "3.5"}, datetime.timedelta(days=428.5)),
+ (
+ {"TnH": "4", "TnM": "54", "TnS": "6.5"},
+ datetime.timedelta(hours=4, minutes=54, seconds=6.5),
+ ),
+ (
+ {"TnH": "1", "TnM": "3", "TnS": "11.858714"},
+ datetime.timedelta(hours=1, minutes=3, seconds=11, microseconds=858714),
+ ),
+ (
+ {"TnH": "4", "TnM": "54", "TnS": "28.512400"},
+ datetime.timedelta(
+ hours=4, minutes=54, seconds=28, microseconds=512400
+ ),
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ # https://bitbucket.org/nielsenb/aniso8601/issues/21/sub-microsecond-precision-is-lost-when
+ (
+ {"PnY": "1999.9999999999999999"},
+ datetime.timedelta(days=729999, seconds=86399, microseconds=999999),
+ ),
+ (
+ {"PnM": "1.9999999999999999"},
+ datetime.timedelta(
+ days=59, hours=23, minutes=59, seconds=59, microseconds=999999
+ ),
+ ),
+ (
+ {"PnW": "1.9999999999999999"},
+ datetime.timedelta(
+ days=13, hours=23, minutes=59, seconds=59, microseconds=999999
+ ),
+ ),
+ (
+ {"PnD": "1.9999999999999999"},
+ datetime.timedelta(
+ days=1, hours=23, minutes=59, seconds=59, microseconds=999999
+ ),
+ ),
+ (
+ {"TnH": "14.9999999999999999"},
+ datetime.timedelta(
+ hours=14, minutes=59, seconds=59, microseconds=999999
+ ),
+ ),
+ ({"TnM": "0.00000000999"}, datetime.timedelta(0)),
+ ({"TnM": "0.0000000999"}, datetime.timedelta(microseconds=5)),
+ ({"TnS": "0.0000001"}, datetime.timedelta(0)),
+ ({"TnS": "2.0000048"}, datetime.timedelta(seconds=2, microseconds=4)),
+ ({"PnY": "1"}, datetime.timedelta(days=365)),
+ ({"PnY": "1.5"}, datetime.timedelta(days=547.5)),
+ ({"PnM": "1"}, datetime.timedelta(days=30)),
+ ({"PnM": "1.5"}, datetime.timedelta(days=45)),
+ ({"PnW": "1"}, datetime.timedelta(days=7)),
+ ({"PnW": "1.5"}, datetime.timedelta(days=10.5)),
+ ({"PnD": "1"}, datetime.timedelta(days=1)),
+ ({"PnD": "1.5"}, datetime.timedelta(days=1.5)),
+ (
+ {
+ "PnY": "0003",
+ "PnM": "06",
+ "PnD": "04",
+ "TnH": "12",
+ "TnM": "30",
+ "TnS": "05",
+ },
+ datetime.timedelta(days=1279, hours=12, minutes=30, seconds=5),
+ ),
+ (
+ {
+ "PnY": "0003",
+ "PnM": "06",
+ "PnD": "04",
+ "TnH": "12",
+ "TnM": "30",
+ "TnS": "05.5",
+ },
+ datetime.timedelta(days=1279, hours=12, minutes=30, seconds=5.5),
+ ),
+ # Test timedelta limit
+ (
+ {"PnD": "999999999", "TnH": "23", "TnM": "59", "TnS": "59.999999"},
+ datetime.timedelta.max,
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ (
+ {
+ "PnY": "0001",
+ "PnM": "02",
+ "PnD": "03",
+ "TnH": "14",
+ "TnM": "43",
+ "TnS": "59.9999997",
+ },
+ datetime.timedelta(
+ days=428, hours=14, minutes=43, seconds=59, microseconds=999999
+ ),
+ ),
+ # Verify overflows
+ ({"TnH": "36"}, datetime.timedelta(days=1, hours=12)),
+ )
+
+ for testtuple in testtuples:
+ result = PythonTimeBuilder.build_duration(**testtuple[0])
+ self.assertEqual(result, testtuple[1])
+
+ def test_build_interval(self):
+ testtuples = (
+ (
+ {
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "duration": DurationTuple(None, "1", None, None, None, None, None),
+ },
+ datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1),
+ datetime.datetime(year=1981, month=3, day=6, hour=1, minute=1),
+ ),
+ (
+ {
+ "end": DateTuple("1981", "04", "05", None, None, None),
+ "duration": DurationTuple(None, "1", None, None, None, None, None),
+ },
+ datetime.date(year=1981, month=4, day=5),
+ datetime.date(year=1981, month=3, day=6),
+ ),
+ (
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ "1.5", None, None, None, None, None, None
+ ),
+ },
+ datetime.date(year=2018, month=3, day=6),
+ datetime.datetime(year=2016, month=9, day=4, hour=12),
+ ),
+ (
+ {
+ "end": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "1", None, None),
+ },
+ datetime.date(year=2014, month=11, day=12),
+ datetime.datetime(year=2014, month=11, day=11, hour=23),
+ ),
+ (
+ {
+ "end": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "4", "54", "6.5"),
+ },
+ datetime.date(year=2014, month=11, day=12),
+ datetime.datetime(
+ year=2014,
+ month=11,
+ day=11,
+ hour=19,
+ minute=5,
+ second=53,
+ microsecond=500000,
+ ),
+ ),
+ (
+ {
+ "end": DatetimeTuple(
+ DateTuple("2050", "03", "01", None, None, None),
+ TimeTuple(
+ "13",
+ "00",
+ "00",
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ "duration": DurationTuple(None, None, None, None, "10", None, None),
+ },
+ datetime.datetime(
+ year=2050,
+ month=3,
+ day=1,
+ hour=13,
+ tzinfo=UTCOffset(name="UTC", minutes=0),
+ ),
+ datetime.datetime(
+ year=2050,
+ month=3,
+ day=1,
+ hour=3,
+ tzinfo=UTCOffset(name="UTC", minutes=0),
+ ),
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ # https://bitbucket.org/nielsenb/aniso8601/issues/21/sub-microsecond-precision-is-lost-when
+ (
+ {
+ "end": DateTuple("2000", "01", "01", None, None, None),
+ "duration": DurationTuple(
+ "1999.9999999999999999", None, None, None, None, None, None
+ ),
+ },
+ datetime.date(year=2000, month=1, day=1),
+ datetime.datetime(
+ year=1, month=4, day=30, hour=0, minute=0, second=0, microsecond=1
+ ),
+ ),
+ (
+ {
+ "end": DateTuple("1989", "03", "01", None, None, None),
+ "duration": DurationTuple(
+ None, "1.9999999999999999", None, None, None, None, None
+ ),
+ },
+ datetime.date(year=1989, month=3, day=1),
+ datetime.datetime(
+ year=1988,
+ month=12,
+ day=31,
+ hour=0,
+ minute=0,
+ second=0,
+ microsecond=1,
+ ),
+ ),
+ (
+ {
+ "end": DateTuple("1989", "03", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, "1.9999999999999999", None, None, None, None
+ ),
+ },
+ datetime.date(year=1989, month=3, day=1),
+ datetime.datetime(
+ year=1989,
+ month=2,
+ day=15,
+ hour=0,
+ minute=0,
+ second=0,
+ microsecond=1,
+ ),
+ ),
+ (
+ {
+ "end": DateTuple("1989", "03", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, "1.9999999999999999", None, None, None
+ ),
+ },
+ datetime.date(year=1989, month=3, day=1),
+ datetime.datetime(
+ year=1989,
+ month=2,
+ day=27,
+ hour=0,
+ minute=0,
+ second=0,
+ microsecond=1,
+ ),
+ ),
+ (
+ {
+ "end": DateTuple("2001", "01", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, "14.9999999999999999", None, None
+ ),
+ },
+ datetime.date(year=2001, month=1, day=1),
+ datetime.datetime(
+ year=2000,
+ month=12,
+ day=31,
+ hour=9,
+ minute=0,
+ second=0,
+ microsecond=1,
+ ),
+ ),
+ (
+ {
+ "end": DateTuple("2001", "01", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, "0.00000000999", None
+ ),
+ },
+ datetime.date(year=2001, month=1, day=1),
+ datetime.datetime(year=2001, month=1, day=1),
+ ),
+ (
+ {
+ "end": DateTuple("2001", "01", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, "0.0000000999", None
+ ),
+ },
+ datetime.date(year=2001, month=1, day=1),
+ datetime.datetime(
+ year=2000,
+ month=12,
+ day=31,
+ hour=23,
+ minute=59,
+ second=59,
+ microsecond=999995,
+ ),
+ ),
+ (
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "0.0000001"
+ ),
+ },
+ datetime.date(year=2018, month=3, day=6),
+ datetime.datetime(year=2018, month=3, day=6),
+ ),
+ (
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "2.0000048"
+ ),
+ },
+ datetime.date(year=2018, month=3, day=6),
+ datetime.datetime(
+ year=2018,
+ month=3,
+ day=5,
+ hour=23,
+ minute=59,
+ second=57,
+ microsecond=999996,
+ ),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "duration": DurationTuple(None, "1", None, "1", None, "1", None),
+ },
+ datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1),
+ datetime.datetime(year=1981, month=5, day=6, hour=1, minute=2),
+ ),
+ (
+ {
+ "start": DateTuple("1981", "04", "05", None, None, None),
+ "duration": DurationTuple(None, "1", None, "1", None, None, None),
+ },
+ datetime.date(year=1981, month=4, day=5),
+ datetime.date(year=1981, month=5, day=6),
+ ),
+ (
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, "2.5", None, None, None, None, None
+ ),
+ },
+ datetime.date(year=2018, month=3, day=6),
+ datetime.date(year=2018, month=5, day=20),
+ ),
+ (
+ {
+ "start": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "1", None, None),
+ },
+ datetime.date(year=2014, month=11, day=12),
+ datetime.datetime(year=2014, month=11, day=12, hour=1, minute=0),
+ ),
+ (
+ {
+ "start": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "4", "54", "6.5"),
+ },
+ datetime.date(year=2014, month=11, day=12),
+ datetime.datetime(
+ year=2014,
+ month=11,
+ day=12,
+ hour=4,
+ minute=54,
+ second=6,
+ microsecond=500000,
+ ),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("2050", "03", "01", None, None, None),
+ TimeTuple(
+ "13",
+ "00",
+ "00",
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ "duration": DurationTuple(None, None, None, None, "10", None, None),
+ },
+ datetime.datetime(
+ year=2050,
+ month=3,
+ day=1,
+ hour=13,
+ tzinfo=UTCOffset(name="UTC", minutes=0),
+ ),
+ datetime.datetime(
+ year=2050,
+ month=3,
+ day=1,
+ hour=23,
+ tzinfo=UTCOffset(name="UTC", minutes=0),
+ ),
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ (
+ {
+ "start": DateTuple("0001", "01", "01", None, None, None),
+ "duration": DurationTuple(
+ "1999.9999999999999999", None, None, None, None, None, None
+ ),
+ },
+ datetime.date(year=1, month=1, day=1),
+ datetime.datetime(
+ year=1999,
+ month=9,
+ day=3,
+ hour=23,
+ minute=59,
+ second=59,
+ microsecond=999999,
+ ),
+ ),
+ (
+ {
+ "start": DateTuple("1989", "03", "01", None, None, None),
+ "duration": DurationTuple(
+ None, "1.9999999999999999", None, None, None, None, None
+ ),
+ },
+ datetime.date(year=1989, month=3, day=1),
+ datetime.datetime(
+ year=1989,
+ month=4,
+ day=29,
+ hour=23,
+ minute=59,
+ second=59,
+ microsecond=999999,
+ ),
+ ),
+ (
+ {
+ "start": DateTuple("1989", "03", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, "1.9999999999999999", None, None, None, None
+ ),
+ },
+ datetime.date(year=1989, month=3, day=1),
+ datetime.datetime(
+ year=1989,
+ month=3,
+ day=14,
+ hour=23,
+ minute=59,
+ second=59,
+ microsecond=999999,
+ ),
+ ),
+ (
+ {
+ "start": DateTuple("1989", "03", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, "1.9999999999999999", None, None, None
+ ),
+ },
+ datetime.date(year=1989, month=3, day=1),
+ datetime.datetime(
+ year=1989,
+ month=3,
+ day=2,
+ hour=23,
+ minute=59,
+ second=59,
+ microsecond=999999,
+ ),
+ ),
+ (
+ {
+ "start": DateTuple("2001", "01", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, "14.9999999999999999", None, None
+ ),
+ },
+ datetime.date(year=2001, month=1, day=1),
+ datetime.datetime(
+ year=2001,
+ month=1,
+ day=1,
+ hour=14,
+ minute=59,
+ second=59,
+ microsecond=999999,
+ ),
+ ),
+ (
+ {
+ "start": DateTuple("2001", "01", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, "0.00000000999", None
+ ),
+ },
+ datetime.date(year=2001, month=1, day=1),
+ datetime.datetime(year=2001, month=1, day=1),
+ ),
+ (
+ {
+ "start": DateTuple("2001", "01", "01", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, "0.0000000999", None
+ ),
+ },
+ datetime.date(year=2001, month=1, day=1),
+ datetime.datetime(
+ year=2001, month=1, day=1, hour=0, minute=0, second=0, microsecond=5
+ ),
+ ),
+ (
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "0.0000001"
+ ),
+ },
+ datetime.date(year=2018, month=3, day=6),
+ datetime.datetime(year=2018, month=3, day=6),
+ ),
+ (
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "2.0000048"
+ ),
+ },
+ datetime.date(year=2018, month=3, day=6),
+ datetime.datetime(
+ year=2018, month=3, day=6, hour=0, minute=0, second=2, microsecond=4
+ ),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ },
+ datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1),
+ datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DateTuple("1981", "04", "05", None, None, None),
+ },
+ datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1),
+ datetime.date(year=1981, month=4, day=5),
+ ),
+ (
+ {
+ "start": DateTuple("1980", "03", "05", None, None, None),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ },
+ datetime.date(year=1980, month=3, day=5),
+ datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1),
+ ),
+ (
+ {
+ "start": DateTuple("1980", "03", "05", None, None, None),
+ "end": DateTuple("1981", "04", "05", None, None, None),
+ },
+ datetime.date(year=1980, month=3, day=5),
+ datetime.date(year=1981, month=4, day=5),
+ ),
+ (
+ {
+ "start": DateTuple("1981", "04", "05", None, None, None),
+ "end": DateTuple("1980", "03", "05", None, None, None),
+ },
+ datetime.date(year=1981, month=4, day=5),
+ datetime.date(year=1980, month=3, day=5),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("2050", "03", "01", None, None, None),
+ TimeTuple(
+ "13",
+ "00",
+ "00",
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("2050", "05", "11", None, None, None),
+ TimeTuple(
+ "15",
+ "30",
+ "00",
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ },
+ datetime.datetime(
+ year=2050,
+ month=3,
+ day=1,
+ hour=13,
+ tzinfo=UTCOffset(name="UTC", minutes=0),
+ ),
+ datetime.datetime(
+ year=2050,
+ month=5,
+ day=11,
+ hour=15,
+ minute=30,
+ tzinfo=UTCOffset(name="UTC", minutes=0),
+ ),
+ ),
+ # Test concise representation
+ (
+ {
+ "start": DateTuple("2020", "01", "01", None, None, None),
+ "end": DateTuple(None, None, "02", None, None, None),
+ },
+ datetime.date(year=2020, month=1, day=1),
+ datetime.date(year=2020, month=1, day=2),
+ ),
+ (
+ {
+ "start": DateTuple("2008", "02", "15", None, None, None),
+ "end": DateTuple(None, "03", "14", None, None, None),
+ },
+ datetime.date(year=2008, month=2, day=15),
+ datetime.date(year=2008, month=3, day=14),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "12", "14", None, None, None),
+ TimeTuple("13", "30", None, None),
+ ),
+ "end": TimeTuple("15", "30", None, None),
+ },
+ datetime.datetime(year=2007, month=12, day=14, hour=13, minute=30),
+ datetime.datetime(year=2007, month=12, day=14, hour=15, minute=30),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("09", "00", None, None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ },
+ datetime.datetime(year=2007, month=11, day=13, hour=9),
+ datetime.datetime(year=2007, month=11, day=15, hour=17),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple(None, None, "16", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ },
+ datetime.datetime(year=2007, month=11, day=13),
+ datetime.datetime(year=2007, month=11, day=16),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple(
+ "09",
+ "00",
+ None,
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ "end": DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ },
+ datetime.datetime(
+ year=2007,
+ month=11,
+ day=13,
+ hour=9,
+ tzinfo=UTCOffset(name="UTC", minutes=0),
+ ),
+ datetime.datetime(
+ year=2007,
+ month=11,
+ day=15,
+ hour=17,
+ tzinfo=UTCOffset(name="UTC", minutes=0),
+ ),
+ ),
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("09", "00", None, None),
+ ),
+ "end": TimeTuple("12", "34.567", None, None),
+ },
+ datetime.datetime(year=2007, month=11, day=13, hour=9),
+ datetime.datetime(
+ year=2007,
+ month=11,
+ day=13,
+ hour=12,
+ minute=34,
+ second=34,
+ microsecond=20000,
+ ),
+ ),
+ (
+ {
+ "start": DateTuple("2007", "11", "13", None, None, None),
+ "end": TimeTuple("12", "34", None, None),
+ },
+ datetime.date(year=2007, month=11, day=13),
+ datetime.datetime(year=2007, month=11, day=13, hour=12, minute=34),
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ (
+ {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00.0000001", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("14", "43", "59.9999997", None),
+ ),
+ },
+ datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1),
+ datetime.datetime(
+ year=1981,
+ month=4,
+ day=5,
+ hour=14,
+ minute=43,
+ second=59,
+ microsecond=999999,
+ ),
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = PythonTimeBuilder.build_interval(**testtuple[0])
+ self.assertEqual(result[0], testtuple[1])
+ self.assertEqual(result[1], testtuple[2])
+
+ def test_build_repeating_interval(self):
+ args = {
+ "Rnn": "3",
+ "interval": IntervalTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ None,
+ DurationTuple(None, None, None, "1", None, None, None),
+ ),
+ }
+ results = list(PythonTimeBuilder.build_repeating_interval(**args))
+
+ self.assertEqual(results[0], datetime.date(year=1981, month=4, day=5))
+ self.assertEqual(results[1], datetime.date(year=1981, month=4, day=6))
+ self.assertEqual(results[2], datetime.date(year=1981, month=4, day=7))
+
+ args = {
+ "Rnn": "11",
+ "interval": IntervalTuple(
+ None,
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DurationTuple(None, None, None, None, "1", "2", None),
+ ),
+ }
+ results = list(PythonTimeBuilder.build_repeating_interval(**args))
+
+ for dateindex in compat.range(0, 11):
+ self.assertEqual(
+ results[dateindex],
+ datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1)
+ - dateindex * datetime.timedelta(hours=1, minutes=2),
+ )
+
+ args = {
+ "Rnn": "2",
+ "interval": IntervalTuple(
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ None,
+ ),
+ }
+ results = list(PythonTimeBuilder.build_repeating_interval(**args))
+
+ self.assertEqual(
+ results[0], datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1)
+ )
+ self.assertEqual(
+ results[1], datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1)
+ )
+
+ args = {
+ "Rnn": "2",
+ "interval": IntervalTuple(
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ None,
+ ),
+ }
+ results = list(PythonTimeBuilder.build_repeating_interval(**args))
+
+ self.assertEqual(
+ results[0], datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1)
+ )
+ self.assertEqual(
+ results[1], datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1)
+ )
+
+ args = {
+ "R": True,
+ "interval": IntervalTuple(
+ None,
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DurationTuple(None, None, None, None, "1", "2", None),
+ ),
+ }
+ resultgenerator = PythonTimeBuilder.build_repeating_interval(**args)
+
+ # Test the first 11 generated
+ for dateindex in compat.range(0, 11):
+ self.assertEqual(
+ next(resultgenerator),
+ datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1)
+ - dateindex * datetime.timedelta(hours=1, minutes=2),
+ )
+
+ args = {
+ "R": True,
+ "interval": IntervalTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ None,
+ DurationTuple(None, None, None, "1", None, None, None),
+ ),
+ }
+ resultgenerator = PythonTimeBuilder.build_repeating_interval(**args)
+
+ # Test the first 11 generated
+ for dateindex in compat.range(0, 11):
+ self.assertEqual(
+ next(resultgenerator),
+ (
+ datetime.datetime(year=1981, month=4, day=5, hour=0, minute=0)
+ + dateindex * datetime.timedelta(days=1)
+ ).date(),
+ )
+
+ def test_build_timezone(self):
+ testtuples = (
+ ({"Z": True, "name": "Z"}, datetime.timedelta(hours=0), "UTC"),
+ (
+ {"negative": False, "hh": "00", "mm": "00", "name": "+00:00"},
+ datetime.timedelta(hours=0),
+ "+00:00",
+ ),
+ (
+ {"negative": False, "hh": "01", "mm": "00", "name": "+01:00"},
+ datetime.timedelta(hours=1),
+ "+01:00",
+ ),
+ (
+ {"negative": True, "hh": "01", "mm": "00", "name": "-01:00"},
+ -datetime.timedelta(hours=1),
+ "-01:00",
+ ),
+ (
+ {"negative": False, "hh": "00", "mm": "12", "name": "+00:12"},
+ datetime.timedelta(minutes=12),
+ "+00:12",
+ ),
+ (
+ {"negative": False, "hh": "01", "mm": "23", "name": "+01:23"},
+ datetime.timedelta(hours=1, minutes=23),
+ "+01:23",
+ ),
+ (
+ {"negative": True, "hh": "01", "mm": "23", "name": "-01:23"},
+ -datetime.timedelta(hours=1, minutes=23),
+ "-01:23",
+ ),
+ (
+ {"negative": False, "hh": "00", "name": "+00"},
+ datetime.timedelta(hours=0),
+ "+00",
+ ),
+ (
+ {"negative": False, "hh": "01", "name": "+01"},
+ datetime.timedelta(hours=1),
+ "+01",
+ ),
+ (
+ {"negative": True, "hh": "01", "name": "-01"},
+ -datetime.timedelta(hours=1),
+ "-01",
+ ),
+ (
+ {"negative": False, "hh": "12", "name": "+12"},
+ datetime.timedelta(hours=12),
+ "+12",
+ ),
+ (
+ {"negative": True, "hh": "12", "name": "-12"},
+ -datetime.timedelta(hours=12),
+ "-12",
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = PythonTimeBuilder.build_timezone(**testtuple[0])
+ self.assertEqual(result.utcoffset(None), testtuple[1])
+ self.assertEqual(result.tzname(None), testtuple[2])
+
+ def test_range_check_date(self):
+ # 0 isn't a valid year for a Python builder
+ with self.assertRaises(YearOutOfBoundsError):
+ PythonTimeBuilder.build_date(YYYY="0000")
+
+ # Leap year
+ # https://bitbucket.org/nielsenb/aniso8601/issues/14/parsing-ordinal-dates-should-only-allow
+ with self.assertRaises(DayOutOfBoundsError):
+ PythonTimeBuilder.build_date(YYYY="1981", DDD="366")
+
+ def test_range_check_time(self):
+ # Hour 24 can only represent midnight
+ with self.assertRaises(MidnightBoundsError):
+ PythonTimeBuilder.build_time(hh="24", mm="00", ss="01")
+
+ with self.assertRaises(MidnightBoundsError):
+ PythonTimeBuilder.build_time(hh="24", mm="00.1")
+
+ with self.assertRaises(MidnightBoundsError):
+ PythonTimeBuilder.build_time(hh="24", mm="01")
+
+ with self.assertRaises(MidnightBoundsError):
+ PythonTimeBuilder.build_time(hh="24.1")
+
+ def test_range_check_duration(self):
+ with self.assertRaises(YearOutOfBoundsError):
+ PythonTimeBuilder.build_duration(
+ PnY=str((datetime.timedelta.max.days // 365) + 1)
+ )
+
+ with self.assertRaises(MonthOutOfBoundsError):
+ PythonTimeBuilder.build_duration(
+ PnM=str((datetime.timedelta.max.days // 30) + 1)
+ )
+
+ with self.assertRaises(DayOutOfBoundsError):
+ PythonTimeBuilder.build_duration(PnD=str(datetime.timedelta.max.days + 1))
+
+ with self.assertRaises(WeekOutOfBoundsError):
+ PythonTimeBuilder.build_duration(
+ PnW=str((datetime.timedelta.max.days // 7) + 1)
+ )
+
+ with self.assertRaises(HoursOutOfBoundsError):
+ PythonTimeBuilder.build_duration(
+ TnH=str((datetime.timedelta.max.days * 24) + 24)
+ )
+
+ with self.assertRaises(MinutesOutOfBoundsError):
+ PythonTimeBuilder.build_duration(
+ TnM=str((datetime.timedelta.max.days * 24 * 60) + 24 * 60)
+ )
+
+ with self.assertRaises(SecondsOutOfBoundsError):
+ PythonTimeBuilder.build_duration(
+ TnS=str((datetime.timedelta.max.days * 24 * 60 * 60) + 24 * 60 * 60)
+ )
+
+ # Split max range across all parts
+ maxpart = datetime.timedelta.max.days // 7
+
+ with self.assertRaises(DayOutOfBoundsError):
+ PythonTimeBuilder.build_duration(
+ PnY=str((maxpart // 365) + 1),
+ PnM=str((maxpart // 30) + 1),
+ PnD=str((maxpart + 1)),
+ PnW=str((maxpart // 7) + 1),
+ TnH=str((maxpart * 24) + 1),
+ TnM=str((maxpart * 24 * 60) + 1),
+ TnS=str((maxpart * 24 * 60 * 60) + 1),
+ )
+
+ def test_range_check_interval(self):
+ with self.assertRaises(YearOutOfBoundsError):
+ PythonTimeBuilder.build_interval(
+ start=DateTuple("0007", None, None, None, None, None),
+ duration=DurationTuple(
+ None, None, None, str(datetime.timedelta.max.days), None, None, None
+ ),
+ )
+
+ with self.assertRaises(YearOutOfBoundsError):
+ PythonTimeBuilder.build_interval(
+ start=DatetimeTuple(
+ DateTuple("0007", None, None, None, None, None),
+ TimeTuple("1", None, None, None),
+ ),
+ duration=DurationTuple(
+ str(datetime.timedelta.max.days // 365),
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ ),
+ )
+
+ with self.assertRaises(YearOutOfBoundsError):
+ PythonTimeBuilder.build_interval(
+ end=DateTuple("0001", None, None, None, None, None),
+ duration=DurationTuple("3", None, None, None, None, None, None),
+ )
+
+ with self.assertRaises(YearOutOfBoundsError):
+ PythonTimeBuilder.build_interval(
+ end=DatetimeTuple(
+ DateTuple("0001", None, None, None, None, None),
+ TimeTuple("1", None, None, None),
+ ),
+ duration=DurationTuple("2", None, None, None, None, None, None),
+ )
+
+ def test_build_week_date(self):
+ weekdate = PythonTimeBuilder._build_week_date(2009, 1)
+ self.assertEqual(weekdate, datetime.date(year=2008, month=12, day=29))
+
+ weekdate = PythonTimeBuilder._build_week_date(2009, 53, isoday=7)
+ self.assertEqual(weekdate, datetime.date(year=2010, month=1, day=3))
+
+ def test_build_ordinal_date(self):
+ ordinaldate = PythonTimeBuilder._build_ordinal_date(1981, 95)
+ self.assertEqual(ordinaldate, datetime.date(year=1981, month=4, day=5))
+
+ def test_iso_year_start(self):
+ yearstart = PythonTimeBuilder._iso_year_start(2004)
+ self.assertEqual(yearstart, datetime.date(year=2003, month=12, day=29))
+
+ yearstart = PythonTimeBuilder._iso_year_start(2010)
+ self.assertEqual(yearstart, datetime.date(year=2010, month=1, day=4))
+
+ yearstart = PythonTimeBuilder._iso_year_start(2009)
+ self.assertEqual(yearstart, datetime.date(year=2008, month=12, day=29))
+
+ def test_date_generator(self):
+ startdate = datetime.date(year=2018, month=8, day=29)
+ timedelta = datetime.timedelta(days=1)
+ iterations = 10
+
+ generator = PythonTimeBuilder._date_generator(startdate, timedelta, iterations)
+
+ results = list(generator)
+
+ for dateindex in compat.range(0, 10):
+ self.assertEqual(
+ results[dateindex],
+ datetime.date(year=2018, month=8, day=29)
+ + dateindex * datetime.timedelta(days=1),
+ )
+
+ def test_date_generator_unbounded(self):
+ startdate = datetime.date(year=2018, month=8, day=29)
+ timedelta = datetime.timedelta(days=5)
+
+ generator = PythonTimeBuilder._date_generator_unbounded(startdate, timedelta)
+
+ # Check the first 10 results
+ for dateindex in compat.range(0, 10):
+ self.assertEqual(
+ next(generator),
+ datetime.date(year=2018, month=8, day=29)
+ + dateindex * datetime.timedelta(days=5),
+ )
+
+ def test_distribute_microseconds(self):
+ self.assertEqual(PythonTimeBuilder._distribute_microseconds(1, (), ()), (1,))
+ self.assertEqual(
+ PythonTimeBuilder._distribute_microseconds(11, (0,), (10,)), (1, 1)
+ )
+ self.assertEqual(
+ PythonTimeBuilder._distribute_microseconds(211, (0, 0), (100, 10)),
+ (2, 1, 1),
+ )
+
+ self.assertEqual(PythonTimeBuilder._distribute_microseconds(1, (), ()), (1,))
+ self.assertEqual(
+ PythonTimeBuilder._distribute_microseconds(11, (5,), (10,)), (6, 1)
+ )
+ self.assertEqual(
+ PythonTimeBuilder._distribute_microseconds(211, (10, 5), (100, 10)),
+ (12, 6, 1),
+ )
diff --git a/libs/aniso8601/compat.py b/libs/aniso8601/compat.py
new file mode 100644
index 000000000..25af5794a
--- /dev/null
+++ b/libs/aniso8601/compat.py
@@ -0,0 +1,24 @@
+# -*- 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.
+
+import sys
+
+PY2 = sys.version_info[0] == 2
+
+if PY2: # pragma: no cover
+ range = xrange # pylint: disable=undefined-variable
+else:
+ range = range
+
+
+def is_string(tocheck):
+ # pylint: disable=undefined-variable
+ if PY2: # pragma: no cover
+ return isinstance(tocheck, str) or isinstance(tocheck, unicode)
+
+ return isinstance(tocheck, str)
diff --git a/libs/aniso8601/date.py b/libs/aniso8601/date.py
new file mode 100644
index 000000000..ea0cf9c59
--- /dev/null
+++ b/libs/aniso8601/date.py
@@ -0,0 +1,161 @@
+# -*- 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 TupleBuilder
+from aniso8601.builders.python import PythonTimeBuilder
+from aniso8601.compat import is_string
+from aniso8601.exceptions import ISOFormatError
+from aniso8601.resolution import DateResolution
+
+
+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
+ isodatetuple = parse_date(isodatestr, builder=TupleBuilder)
+
+ if isodatetuple.DDD is not None:
+ # YYYY-DDD
+ # YYYYDDD
+ return DateResolution.Ordinal
+
+ if isodatetuple.D is not None:
+ # YYYY-Www-D
+ # YYYYWwwD
+ return DateResolution.Weekday
+
+ if isodatetuple.Www is not None:
+ # YYYY-Www
+ # YYYYWww
+ return DateResolution.Week
+
+ if isodatetuple.DD is not None:
+ # YYYY-MM-DD
+ # YYYYMMDD
+ return DateResolution.Day
+
+ if isodatetuple.MM is not None:
+ # YYYY-MM
+ return DateResolution.Month
+
+ # Y[YYY]
+ return DateResolution.Year
+
+
+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,
+ )
diff --git a/libs/aniso8601/decimalfraction.py b/libs/aniso8601/decimalfraction.py
new file mode 100644
index 000000000..3086ee794
--- /dev/null
+++ b/libs/aniso8601/decimalfraction.py
@@ -0,0 +1,12 @@
+# -*- 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.
+
+
+def normalize(value):
+ """Returns the string with decimal separators normalized."""
+ return value.replace(",", ".")
diff --git a/libs/aniso8601/duration.py b/libs/aniso8601/duration.py
new file mode 100644
index 000000000..cdc0f8f7f
--- /dev/null
+++ b/libs/aniso8601/duration.py
@@ -0,0 +1,291 @@
+# -*- 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
diff --git a/libs/aniso8601/exceptions.py b/libs/aniso8601/exceptions.py
new file mode 100644
index 000000000..c14b44421
--- /dev/null
+++ b/libs/aniso8601/exceptions.py
@@ -0,0 +1,51 @@
+# -*- 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.
+
+
+class ISOFormatError(ValueError):
+ """Raised when ISO 8601 string fails a format check."""
+
+
+class RangeCheckError(ValueError):
+ """Parent type of range check errors."""
+
+
+class YearOutOfBoundsError(RangeCheckError):
+ """Raised when year exceeds limits."""
+
+
+class MonthOutOfBoundsError(RangeCheckError):
+ """Raised when month is outside of 1..12."""
+
+
+class WeekOutOfBoundsError(RangeCheckError):
+ """Raised when week exceeds a year."""
+
+
+class DayOutOfBoundsError(RangeCheckError):
+ """Raised when day is outside of 1..365, 1..366 for leap year."""
+
+
+class HoursOutOfBoundsError(RangeCheckError):
+ """Raise when parsed hours are greater than 24."""
+
+
+class MinutesOutOfBoundsError(RangeCheckError):
+ """Raise when parsed seconds are greater than 60."""
+
+
+class SecondsOutOfBoundsError(RangeCheckError):
+ """Raise when parsed seconds are greater than 60."""
+
+
+class MidnightBoundsError(RangeCheckError):
+ """Raise when parsed time has an hour of 24 but is not midnight."""
+
+
+class LeapSecondError(RangeCheckError):
+ """Raised when attempting to parse a leap second"""
diff --git a/libs/aniso8601/interval.py b/libs/aniso8601/interval.py
new file mode 100644
index 000000000..cd0184c6b
--- /dev/null
+++ b/libs/aniso8601/interval.py
@@ -0,0 +1,350 @@
+# -*- 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)
diff --git a/libs/aniso8601/resolution.py b/libs/aniso8601/resolution.py
new file mode 100644
index 000000000..eca2d4e7e
--- /dev/null
+++ b/libs/aniso8601/resolution.py
@@ -0,0 +1,27 @@
+# -*- 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
+
+
+class DateResolution(object):
+ Year, Month, Week, Weekday, Day, Ordinal = list(compat.range(6))
+
+
+class DurationResolution(object):
+ Years, Months, Weeks, Days, Hours, Minutes, Seconds = list(compat.range(7))
+
+
+class IntervalResolution(object):
+ Year, Month, Week, Weekday, Day, Ordinal, Hours, Minutes, Seconds = list(
+ compat.range(9)
+ )
+
+
+class TimeResolution(object):
+ Seconds, Minutes, Hours = list(compat.range(3))
diff --git a/libs/aniso8601/tests/__init__.py b/libs/aniso8601/tests/__init__.py
new file mode 100644
index 000000000..1a94e017a
--- /dev/null
+++ b/libs/aniso8601/tests/__init__.py
@@ -0,0 +1,7 @@
+# -*- 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.
diff --git a/libs/aniso8601/tests/compat.py b/libs/aniso8601/tests/compat.py
new file mode 100644
index 000000000..6c5266589
--- /dev/null
+++ b/libs/aniso8601/tests/compat.py
@@ -0,0 +1,16 @@
+# -*- 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.
+
+import sys
+
+PY2 = sys.version_info[0] == 2
+
+if PY2:
+ import mock # pylint: disable=import-error
+else:
+ from unittest import mock
diff --git a/libs/aniso8601/tests/test_compat.py b/libs/aniso8601/tests/test_compat.py
new file mode 100644
index 000000000..2f7988331
--- /dev/null
+++ b/libs/aniso8601/tests/test_compat.py
@@ -0,0 +1,27 @@
+# -*- 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.
+
+import unittest
+
+from aniso8601.compat import PY2, is_string
+
+
+class TestCompatFunctions(unittest.TestCase):
+ 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({}))
diff --git a/libs/aniso8601/tests/test_date.py b/libs/aniso8601/tests/test_date.py
new file mode 100644
index 000000000..54e3076eb
--- /dev/null
+++ b/libs/aniso8601/tests/test_date.py
@@ -0,0 +1,303 @@
+# -*- 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.
+
+import unittest
+
+import aniso8601
+from aniso8601.date import get_date_resolution, parse_date
+from aniso8601.exceptions import DayOutOfBoundsError, ISOFormatError
+from aniso8601.resolution import DateResolution
+from aniso8601.tests.compat import mock
+
+
+class TestDateResolutionFunctions(unittest.TestCase):
+ def test_get_date_resolution_year(self):
+ self.assertEqual(get_date_resolution("2013"), DateResolution.Year)
+ self.assertEqual(get_date_resolution("0001"), DateResolution.Year)
+ self.assertEqual(get_date_resolution("19"), DateResolution.Year)
+
+ def test_get_date_resolution_month(self):
+ self.assertEqual(get_date_resolution("1981-04"), DateResolution.Month)
+
+ def test_get_date_resolution_week(self):
+ self.assertEqual(get_date_resolution("2004-W53"), DateResolution.Week)
+ self.assertEqual(get_date_resolution("2009-W01"), DateResolution.Week)
+ self.assertEqual(get_date_resolution("2004W53"), DateResolution.Week)
+
+ def test_get_date_resolution_day(self):
+ self.assertEqual(get_date_resolution("2004-04-11"), DateResolution.Day)
+ self.assertEqual(get_date_resolution("20090121"), DateResolution.Day)
+
+ def test_get_date_resolution_year_weekday(self):
+ self.assertEqual(get_date_resolution("2004-W53-6"), DateResolution.Weekday)
+ self.assertEqual(get_date_resolution("2004W536"), DateResolution.Weekday)
+
+ def test_get_date_resolution_year_ordinal(self):
+ self.assertEqual(get_date_resolution("1981-095"), DateResolution.Ordinal)
+ self.assertEqual(get_date_resolution("1981095"), DateResolution.Ordinal)
+
+ def test_get_date_resolution_badtype(self):
+ testtuples = (None, 1, False, 1.234)
+
+ for testtuple in testtuples:
+ with self.assertRaises(ValueError):
+ get_date_resolution(testtuple)
+
+ def test_get_date_resolution_extended_year(self):
+ testtuples = ("+2000", "+30000")
+
+ for testtuple in testtuples:
+ with self.assertRaises(NotImplementedError):
+ get_date_resolution(testtuple)
+
+ def test_get_date_resolution_badweek(self):
+ testtuples = ("2004-W1", "2004W1")
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ get_date_resolution(testtuple)
+
+ def test_get_date_resolution_badweekday(self):
+ testtuples = ("2004-W53-67", "2004W5367")
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ get_date_resolution(testtuple)
+
+ def test_get_date_resolution_badstr(self):
+ testtuples = (
+ "W53",
+ "2004-W",
+ "2014-01-230",
+ "2014-012-23",
+ "201-01-23",
+ "201401230",
+ "201401",
+ "",
+ )
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ get_date_resolution(testtuple)
+
+
+class TestDateParserFunctions(unittest.TestCase):
+ def test_parse_date(self):
+ testtuples = (
+ (
+ "2013",
+ {
+ "YYYY": "2013",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ ),
+ (
+ "0001",
+ {
+ "YYYY": "0001",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ ),
+ (
+ "19",
+ {
+ "YYYY": "19",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ ),
+ (
+ "1981-04-05",
+ {
+ "YYYY": "1981",
+ "MM": "04",
+ "DD": "05",
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ ),
+ (
+ "19810405",
+ {
+ "YYYY": "1981",
+ "MM": "04",
+ "DD": "05",
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ ),
+ (
+ "1981-04",
+ {
+ "YYYY": "1981",
+ "MM": "04",
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ },
+ ),
+ (
+ "2004-W53",
+ {
+ "YYYY": "2004",
+ "MM": None,
+ "DD": None,
+ "Www": "53",
+ "D": None,
+ "DDD": None,
+ },
+ ),
+ (
+ "2009-W01",
+ {
+ "YYYY": "2009",
+ "MM": None,
+ "DD": None,
+ "Www": "01",
+ "D": None,
+ "DDD": None,
+ },
+ ),
+ (
+ "2004-W53-6",
+ {
+ "YYYY": "2004",
+ "MM": None,
+ "DD": None,
+ "Www": "53",
+ "D": "6",
+ "DDD": None,
+ },
+ ),
+ (
+ "2004W53",
+ {
+ "YYYY": "2004",
+ "MM": None,
+ "DD": None,
+ "Www": "53",
+ "D": None,
+ "DDD": None,
+ },
+ ),
+ (
+ "2004W536",
+ {
+ "YYYY": "2004",
+ "MM": None,
+ "DD": None,
+ "Www": "53",
+ "D": "6",
+ "DDD": None,
+ },
+ ),
+ (
+ "1981-095",
+ {
+ "YYYY": "1981",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": "095",
+ },
+ ),
+ (
+ "1981095",
+ {
+ "YYYY": "1981",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": "095",
+ },
+ ),
+ (
+ "1980366",
+ {
+ "YYYY": "1980",
+ "MM": None,
+ "DD": None,
+ "Www": None,
+ "D": None,
+ "DDD": "366",
+ },
+ ),
+ )
+
+ for testtuple in testtuples:
+ with mock.patch.object(
+ aniso8601.date.PythonTimeBuilder, "build_date"
+ ) as mockBuildDate:
+ mockBuildDate.return_value = testtuple[1]
+
+ result = parse_date(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+ mockBuildDate.assert_called_once_with(**testtuple[1])
+
+ def test_parse_date_badtype(self):
+ testtuples = (None, 1, False, 1.234)
+
+ for testtuple in testtuples:
+ with self.assertRaises(ValueError):
+ parse_date(testtuple, builder=None)
+
+ def test_parse_date_badstr(self):
+ testtuples = (
+ "W53",
+ "2004-W",
+ "2014-01-230",
+ "2014-012-23",
+ "201-01-23",
+ "201401230",
+ "201401",
+ "9999 W53",
+ "20.50230",
+ "198104",
+ "bad",
+ "",
+ )
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_date(testtuple, builder=None)
+
+ def test_parse_date_mockbuilder(self):
+ mockBuilder = mock.Mock()
+
+ expectedargs = {
+ "YYYY": "1981",
+ "MM": "04",
+ "DD": "05",
+ "Www": None,
+ "D": None,
+ "DDD": None,
+ }
+
+ mockBuilder.build_date.return_value = expectedargs
+
+ result = parse_date("1981-04-05", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_date.assert_called_once_with(**expectedargs)
diff --git a/libs/aniso8601/tests/test_decimalfraction.py b/libs/aniso8601/tests/test_decimalfraction.py
new file mode 100644
index 000000000..dc52f2406
--- /dev/null
+++ b/libs/aniso8601/tests/test_decimalfraction.py
@@ -0,0 +1,19 @@
+# -*- 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.
+
+import unittest
+
+from aniso8601.decimalfraction import normalize
+
+
+class TestDecimalFractionFunctions(unittest.TestCase):
+ def test_normalize(self):
+ self.assertEqual(normalize(""), "")
+ self.assertEqual(normalize("12.34"), "12.34")
+ self.assertEqual(normalize("123,45"), "123.45")
+ self.assertEqual(normalize("123,45,67"), "123.45.67")
diff --git a/libs/aniso8601/tests/test_duration.py b/libs/aniso8601/tests/test_duration.py
new file mode 100644
index 000000000..0d7d40a4d
--- /dev/null
+++ b/libs/aniso8601/tests/test_duration.py
@@ -0,0 +1,1402 @@
+# -*- 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.
+
+import unittest
+
+import aniso8601
+from aniso8601.duration import (
+ _has_any_component,
+ _parse_duration_combined,
+ _parse_duration_prescribed,
+ _parse_duration_prescribed_notime,
+ _parse_duration_prescribed_time,
+ get_duration_resolution,
+ parse_duration,
+)
+from aniso8601.exceptions import ISOFormatError
+from aniso8601.resolution import DurationResolution
+from aniso8601.tests.compat import mock
+
+
+class TestDurationParserFunctions(unittest.TestCase):
+ def test_get_duration_resolution_years(self):
+ self.assertEqual(get_duration_resolution("P1Y"), DurationResolution.Years)
+ self.assertEqual(get_duration_resolution("P1,5Y"), DurationResolution.Years)
+ self.assertEqual(get_duration_resolution("P1.5Y"), DurationResolution.Years)
+
+ def test_get_duration_resolution_months(self):
+ self.assertEqual(get_duration_resolution("P1Y2M"), DurationResolution.Months)
+ self.assertEqual(get_duration_resolution("P1M"), DurationResolution.Months)
+ self.assertEqual(get_duration_resolution("P1,5M"), DurationResolution.Months)
+ self.assertEqual(get_duration_resolution("P1.5M"), DurationResolution.Months)
+
+ def test_get_duration_resolution_weeks(self):
+ self.assertEqual(get_duration_resolution("P1W"), DurationResolution.Weeks)
+ self.assertEqual(get_duration_resolution("P1,5W"), DurationResolution.Weeks)
+ self.assertEqual(get_duration_resolution("P1.5W"), DurationResolution.Weeks)
+
+ def test_get_duration_resolution_days(self):
+ self.assertEqual(get_duration_resolution("P1Y2M3D"), DurationResolution.Days)
+ self.assertEqual(get_duration_resolution("P1Y2M3,5D"), DurationResolution.Days)
+ self.assertEqual(get_duration_resolution("P1Y2M3.5D"), DurationResolution.Days)
+ self.assertEqual(get_duration_resolution("P1D"), DurationResolution.Days)
+ self.assertEqual(get_duration_resolution("P1,5D"), DurationResolution.Days)
+ self.assertEqual(get_duration_resolution("P1.5D"), DurationResolution.Days)
+
+ def test_get_duration_resolution_hours(self):
+ self.assertEqual(
+ get_duration_resolution("P1Y2M3DT4H"), DurationResolution.Hours
+ )
+ self.assertEqual(get_duration_resolution("PT4H"), DurationResolution.Hours)
+
+ def test_get_duration_resolution_minutes(self):
+ self.assertEqual(
+ get_duration_resolution("P1Y2M3DT4H5M"), DurationResolution.Minutes
+ )
+ self.assertEqual(get_duration_resolution("PT4H5M"), DurationResolution.Minutes)
+
+ def test_get_duration_resolution_seconds(self):
+ self.assertEqual(
+ get_duration_resolution("P1Y2M3DT4H54M6S"), DurationResolution.Seconds
+ )
+ self.assertEqual(
+ get_duration_resolution("P1Y2M3DT4H54M6,5S"), DurationResolution.Seconds
+ )
+ self.assertEqual(
+ get_duration_resolution("P1Y2M3DT4H54M6.5S"), DurationResolution.Seconds
+ )
+ self.assertEqual(
+ get_duration_resolution("PT4H54M6,5S"), DurationResolution.Seconds
+ )
+ self.assertEqual(
+ get_duration_resolution("PT4H54M6.5S"), DurationResolution.Seconds
+ )
+ self.assertEqual(
+ get_duration_resolution("PT0.0000001S"), DurationResolution.Seconds
+ )
+ self.assertEqual(
+ get_duration_resolution("PT2.0000048S"), DurationResolution.Seconds
+ )
+ self.assertEqual(
+ get_duration_resolution("P0003-06-04T12:30:05"), DurationResolution.Seconds
+ )
+ self.assertEqual(
+ get_duration_resolution("P0003-06-04T12:30:05.5"),
+ DurationResolution.Seconds,
+ )
+ self.assertEqual(
+ get_duration_resolution("P0001-02-03T14:43:59.9999997"),
+ DurationResolution.Seconds,
+ )
+
+ def test_parse_duration(self):
+ testtuples = (
+ (
+ "P1Y2M3DT4H54M6S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y2M3DT4H54M6,5S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "P1Y2M3DT4H54M6.5S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "P1YT4H",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": None,
+ "TnS": None,
+ },
+ ),
+ (
+ "P1YT54M",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P1YT6S",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1YT4H54M",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P1YT4H6S",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1YT54M6S",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1YT4H54M6S",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P2MT4H",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": None,
+ "TnS": None,
+ },
+ ),
+ (
+ "P2MT54M",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P2MT6S",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P2MT4H54M",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P2MT4H6S",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P2MT54M6S",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P2MT4H54M6S",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P3DT4H",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": None,
+ "TnS": None,
+ },
+ ),
+ (
+ "P3DT54M",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": None,
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P3DT6S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": None,
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P3DT4H54M",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P3DT4H6S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P3DT54M6S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": None,
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P3DT4H54M6S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y2MT4H",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": None,
+ "TnS": None,
+ },
+ ),
+ (
+ "P1Y2MT54M",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P1Y2MT6S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y2MT4H54M",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P1Y2MT4H6S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y2MT54M6S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y2MT4H54M6S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y3DT4H",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": None,
+ "TnS": None,
+ },
+ ),
+ (
+ "P1Y3DT54M",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": None,
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P1Y3DT6S",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": None,
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y3DT4H54M",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P1Y3DT4H6S",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y3DT54M6S",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": None,
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y3DT4H54M6S",
+ {
+ "PnY": "1",
+ "PnM": None,
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P2M3DT4H",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": None,
+ "TnS": None,
+ },
+ ),
+ (
+ "P2M3DT54M",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": None,
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P2M3DT6S",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": None,
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P2M3DT4H54M",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": None,
+ },
+ ),
+ (
+ "P2M3DT4H6S",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "P2M3DT54M6S",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": None,
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P2M3DT4H54M6S",
+ {
+ "PnY": None,
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "PT4H54M6,5S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "PT4H54M6.5S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "PT4H",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": None,
+ "TnS": None,
+ },
+ ),
+ (
+ "PT5M",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "5",
+ "TnS": None,
+ },
+ ),
+ (
+ "PT6S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "PT1H2M",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "1",
+ "TnM": "2",
+ "TnS": None,
+ },
+ ),
+ (
+ "PT3H4S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "3",
+ "TnM": None,
+ "TnS": "4",
+ },
+ ),
+ (
+ "PT5M6S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "5",
+ "TnS": "6",
+ },
+ ),
+ (
+ "PT0.0000001S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": None,
+ "TnS": "0.0000001",
+ },
+ ),
+ (
+ "PT2.0000048S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": None,
+ "TnS": "2.0000048",
+ },
+ ),
+ ("P1Y", {"PnY": "1", "PnM": None, "PnW": None, "PnD": None}),
+ ("P1,5Y", {"PnY": "1.5", "PnM": None, "PnW": None, "PnD": None}),
+ ("P1.5Y", {"PnY": "1.5", "PnM": None, "PnW": None, "PnD": None}),
+ ("P1M", {"PnY": None, "PnM": "1", "PnW": None, "PnD": None}),
+ ("P1,5M", {"PnY": None, "PnM": "1.5", "PnW": None, "PnD": None}),
+ ("P1.5M", {"PnY": None, "PnM": "1.5", "PnW": None, "PnD": None}),
+ ("P1W", {"PnY": None, "PnM": None, "PnW": "1", "PnD": None}),
+ ("P1,5W", {"PnY": None, "PnM": None, "PnW": "1.5", "PnD": None}),
+ ("P1.5W", {"PnY": None, "PnM": None, "PnW": "1.5", "PnD": None}),
+ ("P1D", {"PnY": None, "PnM": None, "PnW": None, "PnD": "1"}),
+ ("P1,5D", {"PnY": None, "PnM": None, "PnW": None, "PnD": "1.5"}),
+ ("P1.5D", {"PnY": None, "PnM": None, "PnW": None, "PnD": "1.5"}),
+ ("P1Y2M3D", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": "3"}),
+ ("P1Y2M3,5D", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": "3.5"}),
+ ("P1Y2M3.5D", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": "3.5"}),
+ ("P1Y2M", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": None}),
+ (
+ "P0003-06-04T12:30:05",
+ {
+ "PnY": "0003",
+ "PnM": "06",
+ "PnD": "04",
+ "TnH": "12",
+ "TnM": "30",
+ "TnS": "05",
+ },
+ ),
+ (
+ "P0003-06-04T12:30:05.5",
+ {
+ "PnY": "0003",
+ "PnM": "06",
+ "PnD": "04",
+ "TnH": "12",
+ "TnM": "30",
+ "TnS": "05.5",
+ },
+ ),
+ (
+ "P0001-02-03T14:43:59.9999997",
+ {
+ "PnY": "0001",
+ "PnM": "02",
+ "PnD": "03",
+ "TnH": "14",
+ "TnM": "43",
+ "TnS": "59.9999997",
+ },
+ ),
+ )
+
+ for testtuple in testtuples:
+ with mock.patch.object(
+ aniso8601.duration.PythonTimeBuilder, "build_duration"
+ ) as mockBuildDuration:
+ mockBuildDuration.return_value = testtuple[1]
+
+ result = parse_duration(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+ mockBuildDuration.assert_called_once_with(**testtuple[1])
+
+ def test_parse_duration_mockbuilder(self):
+ mockBuilder = mock.Mock()
+
+ expectedargs = {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ }
+
+ mockBuilder.build_duration.return_value = expectedargs
+
+ result = parse_duration("P1Y2M3DT4H54M6S", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_duration.assert_called_once_with(**expectedargs)
+
+ def test_parse_duration_badtype(self):
+ testtuples = (None, 1, False, 1.234)
+
+ for testtuple in testtuples:
+ with self.assertRaises(ValueError):
+ parse_duration(testtuple, builder=None)
+
+ def test_parse_duration_nop(self):
+ with self.assertRaises(ISOFormatError):
+ # Duration must start with a P
+ parse_duration("1Y2M3DT4H54M6S", builder=None)
+
+ def test_parse_duration_weekcombination(self):
+ # Week designator cannot be combined with other time designators
+ # https://bitbucket.org/nielsenb/aniso8601/issues/2/week-designators-should-not-be-combinable
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1Y2W", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1M2W", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P2W3D", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1Y2W3D", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1M2W3D", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1Y1M2W3D", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P7WT4H", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P7WT54M", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P7WT6S", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P7WT4H54M", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P7WT4H6S", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P7WT54M6S", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P7WT4H54M6S", builder=None)
+
+ def test_parse_duration_negative(self):
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P-1Y", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P-2M", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P-3D", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P-T4H", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P-T54M", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P-T6S", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P-7W", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P-1Y2M3DT4H54M6S", builder=None)
+
+ def test_parse_duration_outoforder(self):
+ # Ensure durations are required to be in the correct order
+ # https://bitbucket.org/nielsenb/aniso8601/issues/7/durations-with-time-components-before-t
+ # https://bitbucket.org/nielsenb/aniso8601/issues/8/durations-with-components-in-wrong-order
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1S", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1D1S", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1H1M", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("1Y2M3D1SPT1M", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1Y2M3D2MT1S", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P2M3D1ST1Y1M", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1Y2M2MT3D1S", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("P1D1Y1M", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_duration("PT1S1H", builder=None)
+
+ def test_parse_duration_badstr(self):
+ testtuples = (
+ "PPPPPPPPPPPPPPPPPPPPPPPPPPPP",
+ "PTT",
+ "PX7DDDTX8888UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
+ "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU8888888888888888H$H",
+ "P1Y2M3X.4D",
+ "P1Y2M3.4XD",
+ "P1Y2M3DT4H5M6XS",
+ "PT4H5M6X.2S",
+ "bad",
+ "",
+ )
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_duration(testtuple, builder=None)
+
+ def test_parse_duration_prescribed(self):
+ testtuples = (
+ (
+ "P1Y2M3DT4H54M6S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y2M3DT4H54M6,5S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "P1Y2M3DT4H54M6.5S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "PT4H54M6,5S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "PT4H54M6.5S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ ("P1Y2M3D", {"PnY": "1", "PnM": "2", "" "PnW": None, "PnD": "3"}),
+ ("P1Y2M3,5D", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": "3.5"}),
+ ("P1Y2M3.5D", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": "3.5"}),
+ ("P1Y2M", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": None}),
+ ("P1Y", {"PnY": "1", "PnM": None, "PnW": None, "PnD": None}),
+ ("P1,5Y", {"PnY": "1.5", "PnM": None, "PnW": None, "PnD": None}),
+ ("P1.5Y", {"PnY": "1.5", "PnM": None, "PnW": None, "PnD": None}),
+ ("P1M", {"PnY": None, "PnM": "1", "PnW": None, "PnD": None}),
+ ("P1,5M", {"PnY": None, "PnM": "1.5", "PnW": None, "PnD": None}),
+ ("P1.5M", {"PnY": None, "PnM": "1.5", "PnW": None, "PnD": None}),
+ ("P1W", {"PnY": None, "PnM": None, "PnW": "1", "PnD": None}),
+ ("P1,5W", {"PnY": None, "PnM": None, "PnW": "1.5", "PnD": None}),
+ ("P1.5W", {"PnY": None, "PnM": None, "PnW": "1.5", "PnD": None}),
+ ("P1D", {"PnY": None, "PnM": None, "PnW": None, "PnD": "1"}),
+ ("P1,5D", {"PnY": None, "PnM": None, "PnW": None, "PnD": "1.5"}),
+ ("P1.5D", {"PnY": None, "PnM": None, "PnW": None, "PnD": "1.5"}),
+ )
+
+ for testtuple in testtuples:
+ result = _parse_duration_prescribed(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+
+ def test_parse_duration_prescribed_negative(self):
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P-1Y")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P-2M")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P-3D")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P-4W")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P-1Y2M3D")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P-T1H")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P-T2M")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P-T3S")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P-1Y2M3DT4H54M6S")
+
+ def test_parse_duration_prescribed_multiplefractions(self):
+ with self.assertRaises(ISOFormatError):
+ # Multiple fractions are not allowed
+ _parse_duration_prescribed("P1Y2M3DT4H5.1234M6.1234S")
+
+ def test_parse_duration_prescribed_middlefraction(self):
+ with self.assertRaises(ISOFormatError):
+ # Fraction only allowed on final component
+ _parse_duration_prescribed("P1Y2M3DT4H5.1234M6S")
+
+ def test_parse_duration_prescribed_suffixgarbage(self):
+ # Don't allow garbage after the duration
+ # https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed("P1Dasdfasdf")
+
+ def test_parse_duration_prescribed_notime(self):
+ testtuples = (
+ ("P1Y2M3D", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": "3"}),
+ ("P1Y2M3,5D", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": "3.5"}),
+ ("P1Y2M3.5D", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": "3.5"}),
+ ("P1Y3D", {"PnY": "1", "PnM": None, "PnW": None, "PnD": "3"}),
+ ("P1Y2M", {"PnY": "1", "PnM": "2", "PnW": None, "PnD": None}),
+ ("P2M3D", {"PnY": None, "PnM": "2", "PnW": None, "PnD": "3"}),
+ ("P1Y", {"PnY": "1", "PnM": None, "PnW": None, "PnD": None}),
+ ("P1,5Y", {"PnY": "1.5", "PnM": None, "PnW": None, "PnD": None}),
+ ("P1.5Y", {"PnY": "1.5", "PnM": None, "PnW": None, "PnD": None}),
+ ("P1M", {"PnY": None, "PnM": "1", "PnW": None, "PnD": None}),
+ ("P1,5M", {"PnY": None, "PnM": "1.5", "PnW": None, "PnD": None}),
+ ("P1.5M", {"PnY": None, "PnM": "1.5", "PnW": None, "PnD": None}),
+ ("P1W", {"PnY": None, "PnM": None, "PnW": "1", "PnD": None}),
+ ("P1,5W", {"PnY": None, "PnM": None, "PnW": "1.5", "PnD": None}),
+ ("P1.5W", {"PnY": None, "PnM": None, "PnW": "1.5", "PnD": None}),
+ ("P1D", {"PnY": None, "PnM": None, "PnW": None, "PnD": "1"}),
+ ("P1,5D", {"PnY": None, "PnM": None, "PnW": None, "PnD": "1.5"}),
+ ("P1.5D", {"PnY": None, "PnM": None, "PnW": None, "PnD": "1.5"}),
+ )
+
+ for testtuple in testtuples:
+ result = _parse_duration_prescribed_notime(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+
+ def test_parse_duration_prescribed_notime_timepart(self):
+ # Ensure no time part is allowed
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_notime("P1S")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_notime("P1D1S")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_notime("P1H1M")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_notime("P1Y2M3D4H")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_notime("P1Y2M3D4H5S")
+
+ def test_parse_duration_prescribed_notime_outoforder(self):
+ # Ensure durations are required to be in the correct order
+ # https://bitbucket.org/nielsenb/aniso8601/issues/8/durations-with-components-in-wrong-order
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_notime("P1H1M")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_notime("P1D1Y1M")
+
+ def test_parse_duration_prescribed_notime_badstr(self):
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_notime("P1S")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_notime("P1D1S")
+
+ def test_parse_duration_prescribed_time(self):
+ testtuples = (
+ (
+ "P1Y2M3DT4H54M6S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6",
+ },
+ ),
+ (
+ "P1Y2M3DT4H54M6,5S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "P1Y2M3DT4H54M6.5S",
+ {
+ "PnY": "1",
+ "PnM": "2",
+ "PnW": None,
+ "PnD": "3",
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "PT4H54M6,5S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "PT4H54M6.5S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": "54",
+ "TnS": "6.5",
+ },
+ ),
+ (
+ "PT4H",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "4",
+ "TnM": None,
+ "TnS": None,
+ },
+ ),
+ (
+ "PT5M",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "5",
+ "TnS": None,
+ },
+ ),
+ (
+ "PT6S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": None,
+ "TnS": "6",
+ },
+ ),
+ (
+ "PT1H2M",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "1",
+ "TnM": "2",
+ "TnS": None,
+ },
+ ),
+ (
+ "PT3H4S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": "3",
+ "TnM": None,
+ "TnS": "4",
+ },
+ ),
+ (
+ "PT5M6S",
+ {
+ "PnY": None,
+ "PnM": None,
+ "PnW": None,
+ "PnD": None,
+ "TnH": None,
+ "TnM": "5",
+ "TnS": "6",
+ },
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = _parse_duration_prescribed_time(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+
+ def test_parse_duration_prescribed_time_timeindate(self):
+ # Don't allow time components in date half
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("P1Y2M3D4HT54M6S")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("P1Y2M3D6ST4H54M")
+
+ def test_parse_duration_prescribed_time_dateintime(self):
+ # Don't allow date components in time half
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("P2M3DT1Y4H54M6S")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("P1Y2MT3D4H54M6S")
+
+ def test_parse_duration_prescribed_time_outoforder(self):
+ # Ensure durations are required to be in the correct order
+ # https://bitbucket.org/nielsenb/aniso8601/issues/7/durations-with-time-components-before-t
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("1Y2M3D1SPT1M")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("P1Y2M3D2MT1S")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("P2M3D1ST1Y1M")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("P1Y2M2MT3D1S")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("PT1S1H")
+
+ def test_parse_duration_prescribed_time_badstr(self):
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("P1Y")
+
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_prescribed_time("P1Y1M")
+
+ def test_parse_duration_combined(self):
+ testtuples = (
+ (
+ "P0003-06-04T12:30:05",
+ {
+ "PnY": "0003",
+ "PnM": "06",
+ "PnD": "04",
+ "TnH": "12",
+ "TnM": "30",
+ "TnS": "05",
+ },
+ ),
+ (
+ "P0003-06-04T12:30:05,5",
+ {
+ "PnY": "0003",
+ "PnM": "06",
+ "PnD": "04",
+ "TnH": "12",
+ "TnM": "30",
+ "TnS": "05.5",
+ },
+ ),
+ (
+ "P0003-06-04T12:30:05.5",
+ {
+ "PnY": "0003",
+ "PnM": "06",
+ "PnD": "04",
+ "TnH": "12",
+ "TnM": "30",
+ "TnS": "05.5",
+ },
+ ),
+ (
+ "P0001-02-03T14:43:59.9999997",
+ {
+ "PnY": "0001",
+ "PnM": "02",
+ "PnD": "03",
+ "TnH": "14",
+ "TnM": "43",
+ "TnS": "59.9999997",
+ },
+ ),
+ )
+
+ for testtuple in testtuples:
+ result = _parse_duration_combined(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+
+ def test_parse_duration_combined_suffixgarbage(self):
+ # Don't allow garbage after the duration
+ # https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed
+ with self.assertRaises(ISOFormatError):
+ _parse_duration_combined("P0003-06-04T12:30:05.5asdfasdf")
+
+ def test_has_any_component(self):
+ self.assertTrue(_has_any_component("P1Y", ["Y", "M"]))
+ self.assertFalse(_has_any_component("P1Y", ["M", "D"]))
diff --git a/libs/aniso8601/tests/test_init.py b/libs/aniso8601/tests/test_init.py
new file mode 100644
index 000000000..d5604c6b9
--- /dev/null
+++ b/libs/aniso8601/tests/test_init.py
@@ -0,0 +1,49 @@
+# -*- 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.
+
+import unittest
+
+import aniso8601
+
+
+class TestInitFunctions(unittest.TestCase):
+ def test_import(self):
+ # Verify the function mappings
+ self.assertEqual(aniso8601.parse_datetime, aniso8601.time.parse_datetime)
+ self.assertEqual(aniso8601.parse_time, aniso8601.time.parse_time)
+ self.assertEqual(
+ aniso8601.get_time_resolution, aniso8601.time.get_time_resolution
+ )
+ self.assertEqual(
+ aniso8601.get_datetime_resolution, aniso8601.time.get_datetime_resolution
+ )
+
+ self.assertEqual(aniso8601.parse_date, aniso8601.date.parse_date)
+ self.assertEqual(
+ aniso8601.get_date_resolution, aniso8601.date.get_date_resolution
+ )
+
+ self.assertEqual(aniso8601.parse_duration, aniso8601.duration.parse_duration)
+ self.assertEqual(
+ aniso8601.get_duration_resolution,
+ aniso8601.duration.get_duration_resolution,
+ )
+
+ self.assertEqual(aniso8601.parse_interval, aniso8601.interval.parse_interval)
+ self.assertEqual(
+ aniso8601.parse_repeating_interval,
+ aniso8601.interval.parse_repeating_interval,
+ )
+ self.assertEqual(
+ aniso8601.get_interval_resolution,
+ aniso8601.interval.get_interval_resolution,
+ )
+ self.assertEqual(
+ aniso8601.get_repeating_interval_resolution,
+ aniso8601.interval.get_repeating_interval_resolution,
+ )
diff --git a/libs/aniso8601/tests/test_interval.py b/libs/aniso8601/tests/test_interval.py
new file mode 100644
index 000000000..f01d15112
--- /dev/null
+++ b/libs/aniso8601/tests/test_interval.py
@@ -0,0 +1,1675 @@
+# -*- 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.
+
+import unittest
+
+import aniso8601
+from aniso8601.builders import (
+ DatetimeTuple,
+ DateTuple,
+ DurationTuple,
+ IntervalTuple,
+ TimeTuple,
+ TimezoneTuple,
+)
+from aniso8601.exceptions import ISOFormatError
+from aniso8601.interval import (
+ _get_interval_component_resolution,
+ _get_interval_resolution,
+ _parse_interval,
+ _parse_interval_end,
+ get_interval_resolution,
+ get_repeating_interval_resolution,
+ parse_interval,
+ parse_repeating_interval,
+)
+from aniso8601.resolution import IntervalResolution
+from aniso8601.tests.compat import mock
+
+
+class TestIntervalParser_UtilityFunctions(unittest.TestCase):
+ def test_get_interval_resolution(self):
+ self.assertEqual(
+ _get_interval_resolution(
+ IntervalTuple(
+ start=DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ end=DatetimeTuple(
+ DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ TimeTuple(hh="04", mm="05", ss="06", tz=None),
+ ),
+ duration=None,
+ )
+ ),
+ IntervalResolution.Seconds,
+ )
+ self.assertEqual(
+ _get_interval_resolution(
+ IntervalTuple(
+ start=DatetimeTuple(
+ DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ TimeTuple(hh="04", mm="05", ss="06", tz=None),
+ ),
+ end=DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ duration=None,
+ )
+ ),
+ IntervalResolution.Seconds,
+ )
+
+ self.assertEqual(
+ _get_interval_resolution(
+ IntervalTuple(
+ start=DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ end=None,
+ duration=DurationTuple(
+ PnY="1", PnM="2", PnW=None, PnD="3", TnH="4", TnM="5", TnS="6"
+ ),
+ )
+ ),
+ IntervalResolution.Seconds,
+ )
+ self.assertEqual(
+ _get_interval_resolution(
+ IntervalTuple(
+ start=DatetimeTuple(
+ DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ TimeTuple(hh="04", mm="05", ss="06", tz=None),
+ ),
+ end=None,
+ duration=DurationTuple(
+ PnY="1",
+ PnM="2",
+ PnW=None,
+ PnD="3",
+ TnH=None,
+ TnM=None,
+ TnS=None,
+ ),
+ )
+ ),
+ IntervalResolution.Seconds,
+ )
+
+ self.assertEqual(
+ _get_interval_resolution(
+ IntervalTuple(
+ start=None,
+ end=DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ duration=DurationTuple(
+ PnY="1", PnM="2", PnW=None, PnD="3", TnH="4", TnM="5", TnS="6"
+ ),
+ )
+ ),
+ IntervalResolution.Seconds,
+ )
+ self.assertEqual(
+ _get_interval_resolution(
+ IntervalTuple(
+ start=None,
+ end=DatetimeTuple(
+ DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ TimeTuple(hh="04", mm="05", ss="06", tz=None),
+ ),
+ duration=DurationTuple(
+ PnY="1",
+ PnM="2",
+ PnW=None,
+ PnD="3",
+ TnH=None,
+ TnM=None,
+ TnS=None,
+ ),
+ )
+ ),
+ IntervalResolution.Seconds,
+ )
+
+ def test_get_interval_component_resolution(self):
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DateTuple(YYYY="2001", MM=None, DD=None, Www=None, D=None, DDD="123")
+ ),
+ IntervalResolution.Ordinal,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DateTuple(YYYY="2001", MM=None, DD=None, Www="12", D="3", DDD=None)
+ ),
+ IntervalResolution.Weekday,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DateTuple(YYYY="2001", MM=None, DD=None, Www="12", D=None, DDD=None)
+ ),
+ IntervalResolution.Week,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DateTuple(YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None)
+ ),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DateTuple(YYYY="2001", MM="02", DD=None, Www=None, D=None, DDD=None)
+ ),
+ IntervalResolution.Month,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DateTuple(YYYY="2001", MM=None, DD=None, Www=None, D=None, DDD=None)
+ ),
+ IntervalResolution.Year,
+ )
+
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DatetimeTuple(
+ DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ TimeTuple(hh="04", mm="05", ss="06", tz=None),
+ )
+ ),
+ IntervalResolution.Seconds,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DatetimeTuple(
+ DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ TimeTuple(hh="04", mm="05", ss=None, tz=None),
+ )
+ ),
+ IntervalResolution.Minutes,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DatetimeTuple(
+ DateTuple(
+ YYYY="2001", MM="02", DD="03", Www=None, D=None, DDD=None
+ ),
+ TimeTuple(hh="04", mm=None, ss=None, tz=None),
+ )
+ ),
+ IntervalResolution.Hours,
+ )
+
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DurationTuple(
+ PnY="1", PnM="2", PnW=None, PnD="3", TnH="4", TnM="5", TnS="6"
+ )
+ ),
+ IntervalResolution.Seconds,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DurationTuple(
+ PnY="1", PnM="2", PnW=None, PnD="3", TnH="4", TnM="5", TnS=None
+ )
+ ),
+ IntervalResolution.Minutes,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DurationTuple(
+ PnY="1", PnM="2", PnW=None, PnD="3", TnH="4", TnM=None, TnS=None
+ )
+ ),
+ IntervalResolution.Hours,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DurationTuple(
+ PnY="1", PnM="2", PnW=None, PnD="3", TnH=None, TnM=None, TnS=None
+ )
+ ),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DurationTuple(
+ PnY="1", PnM="2", PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
+ )
+ ),
+ IntervalResolution.Month,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DurationTuple(
+ PnY="1", PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
+ )
+ ),
+ IntervalResolution.Year,
+ )
+ self.assertEqual(
+ _get_interval_component_resolution(
+ DurationTuple(
+ PnY=None, PnM=None, PnW="3", PnD=None, TnH=None, TnM=None, TnS=None
+ )
+ ),
+ IntervalResolution.Week,
+ )
+
+
+class TestIntervalParserFunctions(unittest.TestCase):
+ def test_get_interval_resolution_date(self):
+ self.assertEqual(get_interval_resolution("P1.5Y/2018"), IntervalResolution.Year)
+ self.assertEqual(
+ get_interval_resolution("P1.5Y/2018-03"), IntervalResolution.Month
+ )
+ self.assertEqual(
+ get_interval_resolution("P1.5Y/2018-03-06"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("P1.5Y/2018W01"), IntervalResolution.Week
+ )
+ self.assertEqual(
+ get_interval_resolution("P1.5Y/2018-306"), IntervalResolution.Ordinal
+ )
+ self.assertEqual(
+ get_interval_resolution("P1.5Y/2018W012"), IntervalResolution.Weekday
+ )
+
+ self.assertEqual(get_interval_resolution("2018/P1.5Y"), IntervalResolution.Year)
+ self.assertEqual(
+ get_interval_resolution("2018-03/P1.5Y"), IntervalResolution.Month
+ )
+ self.assertEqual(
+ get_interval_resolution("2018-03-06/P1.5Y"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("2018W01/P1.5Y"), IntervalResolution.Week
+ )
+ self.assertEqual(
+ get_interval_resolution("2018-306/P1.5Y"), IntervalResolution.Ordinal
+ )
+ self.assertEqual(
+ get_interval_resolution("2018W012/P1.5Y"), IntervalResolution.Weekday
+ )
+
+ def test_get_interval_resolution_time(self):
+ self.assertEqual(
+ get_interval_resolution("P1M/1981-04-05T01"), IntervalResolution.Hours
+ )
+ self.assertEqual(
+ get_interval_resolution("P1M/1981-04-05T01:01"), IntervalResolution.Minutes
+ )
+ self.assertEqual(
+ get_interval_resolution("P1M/1981-04-05T01:01:00"),
+ IntervalResolution.Seconds,
+ )
+
+ self.assertEqual(
+ get_interval_resolution("1981-04-05T01/P1M"), IntervalResolution.Hours
+ )
+ self.assertEqual(
+ get_interval_resolution("1981-04-05T01:01/P1M"), IntervalResolution.Minutes
+ )
+ self.assertEqual(
+ get_interval_resolution("1981-04-05T01:01:00/P1M"),
+ IntervalResolution.Seconds,
+ )
+
+ def test_get_interval_resolution_duration(self):
+ self.assertEqual(
+ get_interval_resolution("2014-11-12/P1Y2M3D"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("2014-11-12/P1Y2M"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("2014-11-12/P1Y"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("2014-11-12/P1W"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("2014-11-12/P1Y2M3DT4H"), IntervalResolution.Hours
+ )
+ self.assertEqual(
+ get_interval_resolution("2014-11-12/P1Y2M3DT4H54M"),
+ IntervalResolution.Minutes,
+ )
+ self.assertEqual(
+ get_interval_resolution("2014-11-12/P1Y2M3DT4H54M6S"),
+ IntervalResolution.Seconds,
+ )
+
+ self.assertEqual(
+ get_interval_resolution("P1Y2M3D/2014-11-12"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("P1Y2M/2014-11-12"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("P1Y/2014-11-12"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("P1W/2014-11-12"), IntervalResolution.Day
+ )
+ self.assertEqual(
+ get_interval_resolution("P1Y2M3DT4H/2014-11-12"), IntervalResolution.Hours
+ )
+ self.assertEqual(
+ get_interval_resolution("P1Y2M3DT4H54M/2014-11-12"),
+ IntervalResolution.Minutes,
+ )
+ self.assertEqual(
+ get_interval_resolution("P1Y2M3DT4H54M6S/2014-11-12"),
+ IntervalResolution.Seconds,
+ )
+
+ def test_parse_interval(self):
+ testtuples = (
+ (
+ "P1M/1981-04-05T01:01:00",
+ {
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "duration": DurationTuple(None, "1", None, None, None, None, None),
+ },
+ ),
+ (
+ "P1M/1981-04-05",
+ {
+ "end": DateTuple("1981", "04", "05", None, None, None),
+ "duration": DurationTuple(None, "1", None, None, None, None, None),
+ },
+ ),
+ (
+ "P1,5Y/2018-03-06",
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ "1.5", None, None, None, None, None, None
+ ),
+ },
+ ),
+ (
+ "P1.5Y/2018-03-06",
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ "1.5", None, None, None, None, None, None
+ ),
+ },
+ ),
+ (
+ "PT1H/2014-11-12",
+ {
+ "end": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "1", None, None),
+ },
+ ),
+ (
+ "PT4H54M6.5S/2014-11-12",
+ {
+ "end": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "4", "54", "6.5"),
+ },
+ ),
+ (
+ "PT10H/2050-03-01T13:00:00Z",
+ {
+ "end": DatetimeTuple(
+ DateTuple("2050", "03", "01", None, None, None),
+ TimeTuple(
+ "13",
+ "00",
+ "00",
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ "duration": DurationTuple(None, None, None, None, "10", None, None),
+ },
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ (
+ "PT0.0000001S/2018-03-06",
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "0.0000001"
+ ),
+ },
+ ),
+ (
+ "PT2.0000048S/2018-03-06",
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "2.0000048"
+ ),
+ },
+ ),
+ (
+ "1981-04-05T01:01:00/P1M1DT1M",
+ {
+ "start": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "duration": DurationTuple(None, "1", None, "1", None, "1", None),
+ },
+ ),
+ (
+ "1981-04-05/P1M1D",
+ {
+ "start": DateTuple("1981", "04", "05", None, None, None),
+ "duration": DurationTuple(None, "1", None, "1", None, None, None),
+ },
+ ),
+ (
+ "2018-03-06/P2,5M",
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, "2.5", None, None, None, None, None
+ ),
+ },
+ ),
+ (
+ "2018-03-06/P2.5M",
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, "2.5", None, None, None, None, None
+ ),
+ },
+ ),
+ (
+ "2014-11-12/PT1H",
+ {
+ "start": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "1", None, None),
+ },
+ ),
+ (
+ "2014-11-12/PT4H54M6.5S",
+ {
+ "start": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "4", "54", "6.5"),
+ },
+ ),
+ (
+ "2050-03-01T13:00:00Z/PT10H",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2050", "03", "01", None, None, None),
+ TimeTuple(
+ "13",
+ "00",
+ "00",
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ "duration": DurationTuple(None, None, None, None, "10", None, None),
+ },
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ (
+ "2018-03-06/PT0.0000001S",
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "0.0000001"
+ ),
+ },
+ ),
+ (
+ "2018-03-06/PT2.0000048S",
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "2.0000048"
+ ),
+ },
+ ),
+ (
+ "1980-03-05T01:01:00/1981-04-05T01:01:00",
+ {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ },
+ ),
+ (
+ "1980-03-05T01:01:00/1981-04-05",
+ {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DateTuple("1981", "04", "05", None, None, None),
+ },
+ ),
+ (
+ "1980-03-05/1981-04-05T01:01:00",
+ {
+ "start": DateTuple("1980", "03", "05", None, None, None),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ },
+ ),
+ (
+ "1980-03-05/1981-04-05",
+ {
+ "start": DateTuple("1980", "03", "05", None, None, None),
+ "end": DateTuple("1981", "04", "05", None, None, None),
+ },
+ ),
+ (
+ "1981-04-05/1980-03-05",
+ {
+ "start": DateTuple("1981", "04", "05", None, None, None),
+ "end": DateTuple("1980", "03", "05", None, None, None),
+ },
+ ),
+ (
+ "2050-03-01T13:00:00Z/2050-05-11T15:30:00Z",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2050", "03", "01", None, None, None),
+ TimeTuple(
+ "13",
+ "00",
+ "00",
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("2050", "05", "11", None, None, None),
+ TimeTuple(
+ "15",
+ "30",
+ "00",
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ },
+ ),
+ # Test concise interval
+ (
+ "2020-01-01/02",
+ {
+ "start": DateTuple("2020", "01", "01", None, None, None),
+ "end": DateTuple(None, None, "02", None, None, None),
+ },
+ ),
+ (
+ "2008-02-15/03-14",
+ {
+ "start": DateTuple("2008", "02", "15", None, None, None),
+ "end": DateTuple(None, "03", "14", None, None, None),
+ },
+ ),
+ (
+ "2007-12-14T13:30/15:30",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "12", "14", None, None, None),
+ TimeTuple("13", "30", None, None),
+ ),
+ "end": TimeTuple("15", "30", None, None),
+ },
+ ),
+ (
+ "2007-11-13T09:00/15T17:00",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("09", "00", None, None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ },
+ ),
+ (
+ "2007-11-13T00:00/16T00:00",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple(None, None, "16", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ },
+ ),
+ (
+ "2007-11-13T09:00Z/15T17:00",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple(
+ "09",
+ "00",
+ None,
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ "end": DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ },
+ ),
+ (
+ "2007-11-13T00:00/12:34.567",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ "end": TimeTuple("12", "34.567", None, None),
+ },
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ (
+ "1980-03-05T01:01:00.0000001/" "1981-04-05T14:43:59.9999997",
+ {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00.0000001", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("14", "43", "59.9999997", None),
+ ),
+ },
+ ),
+ )
+
+ for testtuple in testtuples:
+ with mock.patch.object(
+ aniso8601.interval.PythonTimeBuilder, "build_interval"
+ ) as mockBuildInterval:
+ mockBuildInterval.return_value = testtuple[1]
+
+ result = parse_interval(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+ mockBuildInterval.assert_called_once_with(**testtuple[1])
+
+ # Test different separators
+ with mock.patch.object(
+ aniso8601.interval.PythonTimeBuilder, "build_interval"
+ ) as mockBuildInterval:
+ expectedargs = {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ }
+
+ mockBuildInterval.return_value = expectedargs
+
+ result = parse_interval(
+ "1980-03-05T01:01:00--1981-04-05T01:01:00", intervaldelimiter="--"
+ )
+
+ self.assertEqual(result, expectedargs)
+ mockBuildInterval.assert_called_once_with(**expectedargs)
+
+ with mock.patch.object(
+ aniso8601.interval.PythonTimeBuilder, "build_interval"
+ ) as mockBuildInterval:
+ expectedargs = {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ }
+
+ mockBuildInterval.return_value = expectedargs
+
+ result = parse_interval(
+ "1980-03-05 01:01:00/1981-04-05 01:01:00", datetimedelimiter=" "
+ )
+
+ self.assertEqual(result, expectedargs)
+ mockBuildInterval.assert_called_once_with(**expectedargs)
+
+ def test_parse_interval_mockbuilder(self):
+ mockBuilder = mock.Mock()
+
+ expectedargs = {
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "duration": DurationTuple(None, "1", None, None, None, None, None),
+ }
+
+ mockBuilder.build_interval.return_value = expectedargs
+
+ result = parse_interval("P1M/1981-04-05T01:01:00", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_interval.assert_called_once_with(**expectedargs)
+
+ mockBuilder = mock.Mock()
+
+ expectedargs = {
+ "start": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "1", None, None),
+ }
+
+ mockBuilder.build_interval.return_value = expectedargs
+
+ result = parse_interval("2014-11-12/PT1H", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_interval.assert_called_once_with(**expectedargs)
+
+ mockBuilder = mock.Mock()
+
+ expectedargs = {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ }
+
+ mockBuilder.build_interval.return_value = expectedargs
+
+ result = parse_interval(
+ "1980-03-05T01:01:00/1981-04-05T01:01:00", builder=mockBuilder
+ )
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_interval.assert_called_once_with(**expectedargs)
+
+ def test_parse_interval_badtype(self):
+ testtuples = (None, 1, False, 1.234)
+
+ for testtuple in testtuples:
+ with self.assertRaises(ValueError):
+ parse_interval(testtuple, builder=None)
+
+ def test_parse_interval_baddelimiter(self):
+ testtuples = (
+ "1980-03-05T01:01:00,1981-04-05T01:01:00",
+ "P1M 1981-04-05T01:01:00",
+ )
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_interval(testtuple, builder=None)
+
+ def test_parse_interval_badstr(self):
+ testtuples = ("/", "0/0/0", "20.50230/0", "5/%", "1/21", "bad", "")
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_interval(testtuple, builder=None)
+
+ def test_parse_interval_repeating(self):
+ # Parse interval can't parse repeating intervals
+ with self.assertRaises(ISOFormatError):
+ parse_interval("R3/1981-04-05/P1D")
+
+ with self.assertRaises(ISOFormatError):
+ parse_interval("R3/1981-04-05/P0003-06-04T12:30:05.5")
+
+ with self.assertRaises(ISOFormatError):
+ parse_interval("R/PT1H2M/1980-03-05T01:01:00")
+
+ def test_parse_interval_suffixgarbage(self):
+ # Don't allow garbage after the duration
+ # https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed
+ with self.assertRaises(ISOFormatError):
+ parse_interval("2001/P1Dasdf", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_interval("P1Dasdf/2001", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_interval("2001/P0003-06-04T12:30:05.5asdfasdf", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_interval("P0003-06-04T12:30:05.5asdfasdf/2001", builder=None)
+
+ def test_parse_interval_internal(self):
+ # Test the internal _parse_interval function
+ testtuples = (
+ (
+ "P1M/1981-04-05T01:01:00",
+ {
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "duration": DurationTuple(None, "1", None, None, None, None, None),
+ },
+ ),
+ (
+ "P1M/1981-04-05",
+ {
+ "end": DateTuple("1981", "04", "05", None, None, None),
+ "duration": DurationTuple(None, "1", None, None, None, None, None),
+ },
+ ),
+ (
+ "P1,5Y/2018-03-06",
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ "1.5", None, None, None, None, None, None
+ ),
+ },
+ ),
+ (
+ "P1.5Y/2018-03-06",
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ "1.5", None, None, None, None, None, None
+ ),
+ },
+ ),
+ (
+ "PT1H/2014-11-12",
+ {
+ "end": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "1", None, None),
+ },
+ ),
+ (
+ "PT4H54M6.5S/2014-11-12",
+ {
+ "end": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "4", "54", "6.5"),
+ },
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ (
+ "PT0.0000001S/2018-03-06",
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "0.0000001"
+ ),
+ },
+ ),
+ (
+ "PT2.0000048S/2018-03-06",
+ {
+ "end": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "2.0000048"
+ ),
+ },
+ ),
+ (
+ "1981-04-05T01:01:00/P1M1DT1M",
+ {
+ "start": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "duration": DurationTuple(None, "1", None, "1", None, "1", None),
+ },
+ ),
+ (
+ "1981-04-05/P1M1D",
+ {
+ "start": DateTuple("1981", "04", "05", None, None, None),
+ "duration": DurationTuple(None, "1", None, "1", None, None, None),
+ },
+ ),
+ (
+ "2018-03-06/P2,5M",
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, "2.5", None, None, None, None, None
+ ),
+ },
+ ),
+ (
+ "2018-03-06/P2.5M",
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, "2.5", None, None, None, None, None
+ ),
+ },
+ ),
+ (
+ "2014-11-12/PT1H",
+ {
+ "start": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "1", None, None),
+ },
+ ),
+ (
+ "2014-11-12/PT4H54M6.5S",
+ {
+ "start": DateTuple("2014", "11", "12", None, None, None),
+ "duration": DurationTuple(None, None, None, None, "4", "54", "6.5"),
+ },
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ (
+ "2018-03-06/PT0.0000001S",
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "0.0000001"
+ ),
+ },
+ ),
+ (
+ "2018-03-06/PT2.0000048S",
+ {
+ "start": DateTuple("2018", "03", "06", None, None, None),
+ "duration": DurationTuple(
+ None, None, None, None, None, None, "2.0000048"
+ ),
+ },
+ ),
+ (
+ "1980-03-05T01:01:00/1981-04-05T01:01:00",
+ {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ },
+ ),
+ (
+ "1980-03-05T01:01:00/1981-04-05",
+ {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DateTuple("1981", "04", "05", None, None, None),
+ },
+ ),
+ (
+ "1980-03-05/1981-04-05T01:01:00",
+ {
+ "start": DateTuple("1980", "03", "05", None, None, None),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ },
+ ),
+ (
+ "1980-03-05/1981-04-05",
+ {
+ "start": DateTuple("1980", "03", "05", None, None, None),
+ "end": DateTuple("1981", "04", "05", None, None, None),
+ },
+ ),
+ (
+ "1981-04-05/1980-03-05",
+ {
+ "start": DateTuple("1981", "04", "05", None, None, None),
+ "end": DateTuple("1980", "03", "05", None, None, None),
+ },
+ ),
+ # Test concise interval
+ (
+ "2020-01-01/02",
+ {
+ "start": DateTuple("2020", "01", "01", None, None, None),
+ "end": DateTuple(None, None, "02", None, None, None),
+ },
+ ),
+ (
+ "2008-02-15/03-14",
+ {
+ "start": DateTuple("2008", "02", "15", None, None, None),
+ "end": DateTuple(None, "03", "14", None, None, None),
+ },
+ ),
+ (
+ "2007-12-14T13:30/15:30",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "12", "14", None, None, None),
+ TimeTuple("13", "30", None, None),
+ ),
+ "end": TimeTuple("15", "30", None, None),
+ },
+ ),
+ (
+ "2007-11-13T09:00/15T17:00",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("09", "00", None, None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ },
+ ),
+ (
+ "2007-11-13T00:00/16T00:00",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple(None, None, "16", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ },
+ ),
+ (
+ "2007-11-13T09:00Z/15T17:00",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple(
+ "09",
+ "00",
+ None,
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ "end": DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ },
+ ),
+ (
+ "2007-11-13T00:00/12:34.567",
+ {
+ "start": DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ "end": TimeTuple("12", "34.567", None, None),
+ },
+ ),
+ # Make sure we truncate, not round
+ # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
+ (
+ "1980-03-05T01:01:00.0000001/" "1981-04-05T14:43:59.9999997",
+ {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00.0000001", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("14", "43", "59.9999997", None),
+ ),
+ },
+ ),
+ )
+
+ for testtuple in testtuples:
+ mockBuilder = mock.Mock()
+ mockBuilder.build_interval.return_value = testtuple[1]
+
+ result = _parse_interval(testtuple[0], mockBuilder)
+
+ self.assertEqual(result, testtuple[1])
+ mockBuilder.build_interval.assert_called_once_with(**testtuple[1])
+
+ # Test different separators
+ expectedargs = {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ }
+
+ mockBuilder = mock.Mock()
+ mockBuilder.build_interval.return_value = expectedargs
+
+ result = _parse_interval(
+ "1980-03-05T01:01:00--1981-04-05T01:01:00",
+ mockBuilder,
+ intervaldelimiter="--",
+ )
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_interval.assert_called_once_with(**expectedargs)
+
+ expectedargs = {
+ "start": DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ "end": DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ }
+
+ mockBuilder = mock.Mock()
+ mockBuilder.build_interval.return_value = expectedargs
+
+ _parse_interval(
+ "1980-03-05 01:01:00/1981-04-05 01:01:00",
+ mockBuilder,
+ datetimedelimiter=" ",
+ )
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_interval.assert_called_once_with(**expectedargs)
+
+ def test_parse_interval_end(self):
+ self.assertEqual(
+ _parse_interval_end(
+ "02", DateTuple("2020", "01", "01", None, None, None), "T"
+ ),
+ DateTuple(None, None, "02", None, None, None),
+ )
+
+ self.assertEqual(
+ _parse_interval_end(
+ "03-14", DateTuple("2008", "02", "15", None, None, None), "T"
+ ),
+ DateTuple(None, "03", "14", None, None, None),
+ )
+
+ self.assertEqual(
+ _parse_interval_end(
+ "0314", DateTuple("2008", "02", "15", None, None, None), "T"
+ ),
+ DateTuple(None, "03", "14", None, None, None),
+ )
+
+ self.assertEqual(
+ _parse_interval_end(
+ "15:30",
+ DatetimeTuple(
+ DateTuple("2007", "12", "14", None, None, None),
+ TimeTuple("13", "30", None, None),
+ ),
+ "T",
+ ),
+ TimeTuple("15", "30", None, None),
+ )
+
+ self.assertEqual(
+ _parse_interval_end(
+ "15T17:00",
+ DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("09", "00", None, None),
+ ),
+ "T",
+ ),
+ DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ )
+
+ self.assertEqual(
+ _parse_interval_end(
+ "16T00:00",
+ DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ "T",
+ ),
+ DatetimeTuple(
+ DateTuple(None, None, "16", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ )
+
+ self.assertEqual(
+ _parse_interval_end(
+ "15 17:00",
+ DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("09", "00", None, None),
+ ),
+ " ",
+ ),
+ DatetimeTuple(
+ DateTuple(None, None, "15", None, None, None),
+ TimeTuple("17", "00", None, None),
+ ),
+ )
+
+ self.assertEqual(
+ _parse_interval_end(
+ "12:34.567",
+ DatetimeTuple(
+ DateTuple("2007", "11", "13", None, None, None),
+ TimeTuple("00", "00", None, None),
+ ),
+ "T",
+ ),
+ TimeTuple("12", "34.567", None, None),
+ )
+
+
+class TestRepeatingIntervalParserFunctions(unittest.TestCase):
+ def test_get_interval_resolution_date(self):
+ self.assertEqual(
+ get_repeating_interval_resolution("R/P1.5Y/2018"), IntervalResolution.Year
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R1/P1.5Y/2018-03"),
+ IntervalResolution.Month,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R2/P1.5Y/2018-03-06"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R3/P1.5Y/2018W01"),
+ IntervalResolution.Week,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R4/P1.5Y/2018-306"),
+ IntervalResolution.Ordinal,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R5/P1.5Y/2018W012"),
+ IntervalResolution.Weekday,
+ )
+
+ self.assertEqual(
+ get_repeating_interval_resolution("R/2018/P1.5Y"), IntervalResolution.Year
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R1/2018-03/P1.5Y"),
+ IntervalResolution.Month,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R2/2018-03-06/P1.5Y"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R3/2018W01/P1.5Y"),
+ IntervalResolution.Week,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R4/2018-306/P1.5Y"),
+ IntervalResolution.Ordinal,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R5/2018W012/P1.5Y"),
+ IntervalResolution.Weekday,
+ )
+
+ def test_get_interval_resolution_time(self):
+ self.assertEqual(
+ get_repeating_interval_resolution("R/P1M/1981-04-05T01"),
+ IntervalResolution.Hours,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R1/P1M/1981-04-05T01:01"),
+ IntervalResolution.Minutes,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R2/P1M/1981-04-05T01:01:00"),
+ IntervalResolution.Seconds,
+ )
+
+ self.assertEqual(
+ get_repeating_interval_resolution("R/1981-04-05T01/P1M"),
+ IntervalResolution.Hours,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R1/1981-04-05T01:01/P1M"),
+ IntervalResolution.Minutes,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R2/1981-04-05T01:01:00/P1M"),
+ IntervalResolution.Seconds,
+ )
+
+ def test_get_interval_resolution_duration(self):
+ self.assertEqual(
+ get_repeating_interval_resolution("R/2014-11-12/P1Y2M3D"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R1/2014-11-12/P1Y2M"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R2/2014-11-12/P1Y"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R3/2014-11-12/P1W"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R4/2014-11-12/P1Y2M3DT4H"),
+ IntervalResolution.Hours,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R5/2014-11-12/P1Y2M3DT4H54M"),
+ IntervalResolution.Minutes,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R6/2014-11-12/P1Y2M3DT4H54M6S"),
+ IntervalResolution.Seconds,
+ )
+
+ self.assertEqual(
+ get_repeating_interval_resolution("R/P1Y2M3D/2014-11-12"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R1/P1Y2M/2014-11-12"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R2/P1Y/2014-11-12"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R3/P1W/2014-11-12"),
+ IntervalResolution.Day,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R4/P1Y2M3DT4H/2014-11-12"),
+ IntervalResolution.Hours,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R5/P1Y2M3DT4H54M/2014-11-12"),
+ IntervalResolution.Minutes,
+ )
+ self.assertEqual(
+ get_repeating_interval_resolution("R6/P1Y2M3DT4H54M6S/2014-11-12"),
+ IntervalResolution.Seconds,
+ )
+
+ def test_parse_repeating_interval(self):
+ with mock.patch.object(
+ aniso8601.interval.PythonTimeBuilder, "build_repeating_interval"
+ ) as mockBuilder:
+ expectedargs = {
+ "R": False,
+ "Rnn": "3",
+ "interval": IntervalTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ None,
+ DurationTuple(None, None, None, "1", None, None, None),
+ ),
+ }
+
+ mockBuilder.return_value = expectedargs
+
+ result = parse_repeating_interval("R3/1981-04-05/P1D")
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.assert_called_once_with(**expectedargs)
+
+ with mock.patch.object(
+ aniso8601.interval.PythonTimeBuilder, "build_repeating_interval"
+ ) as mockBuilder:
+ expectedargs = {
+ "R": False,
+ "Rnn": "11",
+ "interval": IntervalTuple(
+ None,
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DurationTuple(None, None, None, None, "1", "2", None),
+ ),
+ }
+
+ mockBuilder.return_value = expectedargs
+
+ result = parse_repeating_interval("R11/PT1H2M/1980-03-05T01:01:00")
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.assert_called_once_with(**expectedargs)
+
+ with mock.patch.object(
+ aniso8601.interval.PythonTimeBuilder, "build_repeating_interval"
+ ) as mockBuilder:
+ expectedargs = {
+ "R": False,
+ "Rnn": "2",
+ "interval": IntervalTuple(
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ None,
+ ),
+ }
+
+ mockBuilder.return_value = expectedargs
+
+ result = parse_repeating_interval(
+ "R2--1980-03-05T01:01:00--" "1981-04-05T01:01:00",
+ intervaldelimiter="--",
+ )
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.assert_called_once_with(**expectedargs)
+
+ with mock.patch.object(
+ aniso8601.interval.PythonTimeBuilder, "build_repeating_interval"
+ ) as mockBuilder:
+ expectedargs = {
+ "R": False,
+ "Rnn": "2",
+ "interval": IntervalTuple(
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DatetimeTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ None,
+ ),
+ }
+
+ mockBuilder.return_value = expectedargs
+
+ result = parse_repeating_interval(
+ "R2/" "1980-03-05 01:01:00/" "1981-04-05 01:01:00",
+ datetimedelimiter=" ",
+ )
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.assert_called_once_with(**expectedargs)
+
+ with mock.patch.object(
+ aniso8601.interval.PythonTimeBuilder, "build_repeating_interval"
+ ) as mockBuilder:
+ expectedargs = {
+ "R": True,
+ "Rnn": None,
+ "interval": IntervalTuple(
+ None,
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DurationTuple(None, None, None, None, "1", "2", None),
+ ),
+ }
+
+ mockBuilder.return_value = expectedargs
+
+ result = parse_repeating_interval("R/PT1H2M/1980-03-05T01:01:00")
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.assert_called_once_with(**expectedargs)
+
+ def test_parse_repeating_interval_mockbuilder(self):
+ mockBuilder = mock.Mock()
+
+ args = {
+ "R": False,
+ "Rnn": "3",
+ "interval": IntervalTuple(
+ DateTuple("1981", "04", "05", None, None, None),
+ None,
+ DurationTuple(None, None, None, "1", None, None, None),
+ ),
+ }
+
+ mockBuilder.build_repeating_interval.return_value = args
+
+ result = parse_repeating_interval("R3/1981-04-05/P1D", builder=mockBuilder)
+
+ self.assertEqual(result, args)
+ mockBuilder.build_repeating_interval.assert_called_once_with(**args)
+
+ mockBuilder = mock.Mock()
+
+ args = {
+ "R": False,
+ "Rnn": "11",
+ "interval": IntervalTuple(
+ None,
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DurationTuple(None, None, None, None, "1", "2", None),
+ ),
+ }
+
+ mockBuilder.build_repeating_interval.return_value = args
+
+ result = parse_repeating_interval(
+ "R11/PT1H2M/1980-03-05T01:01:00", builder=mockBuilder
+ )
+
+ self.assertEqual(result, args)
+ mockBuilder.build_repeating_interval.assert_called_once_with(**args)
+
+ mockBuilder = mock.Mock()
+
+ args = {
+ "R": True,
+ "Rnn": None,
+ "interval": IntervalTuple(
+ None,
+ DatetimeTuple(
+ DateTuple("1980", "03", "05", None, None, None),
+ TimeTuple("01", "01", "00", None),
+ ),
+ DurationTuple(None, None, None, None, "1", "2", None),
+ ),
+ }
+
+ mockBuilder.build_repeating_interval.return_value = args
+
+ result = parse_repeating_interval(
+ "R/PT1H2M/1980-03-05T01:01:00", builder=mockBuilder
+ )
+
+ self.assertEqual(result, args)
+ mockBuilder.build_repeating_interval.assert_called_once_with(**args)
+
+ def test_parse_repeating_interval_badtype(self):
+ testtuples = (None, 1, False, 1.234)
+
+ for testtuple in testtuples:
+ with self.assertRaises(ValueError):
+ parse_repeating_interval(testtuple, builder=None)
+
+ def test_parse_repeating_interval_baddelimiter(self):
+ testtuples = ("R,PT1H2M,1980-03-05T01:01:00", "R3 1981-04-05 P1D")
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_repeating_interval(testtuple, builder=None)
+
+ def test_parse_repeating_interval_suffixgarbage(self):
+ # Don't allow garbage after the duration
+ # https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed
+ with self.assertRaises(ISOFormatError):
+ parse_repeating_interval("R3/1981-04-05/P1Dasdf", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_repeating_interval(
+ "R3/" "1981-04-05/" "P0003-06-04T12:30:05.5asdfasdf", builder=None
+ )
+
+ def test_parse_repeating_interval_badstr(self):
+ testtuples = ("bad", "")
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_repeating_interval(testtuple, builder=None)
diff --git a/libs/aniso8601/tests/test_time.py b/libs/aniso8601/tests/test_time.py
new file mode 100644
index 000000000..dcee5e03f
--- /dev/null
+++ b/libs/aniso8601/tests/test_time.py
@@ -0,0 +1,539 @@
+# -*- 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.
+
+import unittest
+
+import aniso8601
+from aniso8601.builders import DatetimeTuple, DateTuple, TimeTuple, TimezoneTuple
+from aniso8601.exceptions import ISOFormatError
+from aniso8601.resolution import TimeResolution
+from aniso8601.tests.compat import mock
+from aniso8601.time import (
+ _get_time_resolution,
+ get_datetime_resolution,
+ get_time_resolution,
+ parse_datetime,
+ parse_time,
+)
+
+
+class TestTimeResolutionFunctions(unittest.TestCase):
+ def test_get_time_resolution(self):
+ self.assertEqual(get_time_resolution("01:23:45"), TimeResolution.Seconds)
+ self.assertEqual(get_time_resolution("24:00:00"), TimeResolution.Seconds)
+ self.assertEqual(get_time_resolution("23:21:28,512400"), TimeResolution.Seconds)
+ self.assertEqual(get_time_resolution("23:21:28.512400"), TimeResolution.Seconds)
+ self.assertEqual(get_time_resolution("01:23"), TimeResolution.Minutes)
+ self.assertEqual(get_time_resolution("24:00"), TimeResolution.Minutes)
+ self.assertEqual(get_time_resolution("01:23,4567"), TimeResolution.Minutes)
+ self.assertEqual(get_time_resolution("01:23.4567"), TimeResolution.Minutes)
+ self.assertEqual(get_time_resolution("012345"), TimeResolution.Seconds)
+ self.assertEqual(get_time_resolution("240000"), TimeResolution.Seconds)
+ self.assertEqual(get_time_resolution("0123"), TimeResolution.Minutes)
+ self.assertEqual(get_time_resolution("2400"), TimeResolution.Minutes)
+ self.assertEqual(get_time_resolution("01"), TimeResolution.Hours)
+ self.assertEqual(get_time_resolution("24"), TimeResolution.Hours)
+ self.assertEqual(get_time_resolution("12,5"), TimeResolution.Hours)
+ self.assertEqual(get_time_resolution("12.5"), TimeResolution.Hours)
+ self.assertEqual(
+ get_time_resolution("232128.512400+00:00"), TimeResolution.Seconds
+ )
+ self.assertEqual(get_time_resolution("0123.4567+00:00"), TimeResolution.Minutes)
+ self.assertEqual(get_time_resolution("01.4567+00:00"), TimeResolution.Hours)
+ self.assertEqual(get_time_resolution("01:23:45+00:00"), TimeResolution.Seconds)
+ self.assertEqual(get_time_resolution("24:00:00+00:00"), TimeResolution.Seconds)
+ self.assertEqual(
+ get_time_resolution("23:21:28.512400+00:00"), TimeResolution.Seconds
+ )
+ self.assertEqual(get_time_resolution("01:23+00:00"), TimeResolution.Minutes)
+ self.assertEqual(get_time_resolution("24:00+00:00"), TimeResolution.Minutes)
+ self.assertEqual(
+ get_time_resolution("01:23.4567+00:00"), TimeResolution.Minutes
+ )
+ self.assertEqual(
+ get_time_resolution("23:21:28.512400+11:15"), TimeResolution.Seconds
+ )
+ self.assertEqual(
+ get_time_resolution("23:21:28.512400-12:34"), TimeResolution.Seconds
+ )
+ self.assertEqual(
+ get_time_resolution("23:21:28.512400Z"), TimeResolution.Seconds
+ )
+ self.assertEqual(
+ get_time_resolution("06:14:00.000123Z"), TimeResolution.Seconds
+ )
+
+ def test_get_datetime_resolution(self):
+ self.assertEqual(
+ get_datetime_resolution("2019-06-05T01:03:11.858714"),
+ TimeResolution.Seconds,
+ )
+ self.assertEqual(
+ get_datetime_resolution("2019-06-05T01:03:11"), TimeResolution.Seconds
+ )
+ self.assertEqual(
+ get_datetime_resolution("2019-06-05T01:03"), TimeResolution.Minutes
+ )
+ self.assertEqual(get_datetime_resolution("2019-06-05T01"), TimeResolution.Hours)
+
+ def test_get_time_resolution_badtype(self):
+ testtuples = (None, 1, False, 1.234)
+
+ for testtuple in testtuples:
+ with self.assertRaises(ValueError):
+ get_time_resolution(testtuple)
+
+ def test_get_time_resolution_badstr(self):
+ testtuples = ("A6:14:00.000123Z", "06:14:0B", "bad", "")
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ get_time_resolution(testtuple)
+
+ def test_get_time_resolution_internal(self):
+ self.assertEqual(
+ _get_time_resolution(TimeTuple(hh="01", mm="02", ss="03", tz=None)),
+ TimeResolution.Seconds,
+ )
+ self.assertEqual(
+ _get_time_resolution(TimeTuple(hh="01", mm="02", ss=None, tz=None)),
+ TimeResolution.Minutes,
+ )
+ self.assertEqual(
+ _get_time_resolution(TimeTuple(hh="01", mm=None, ss=None, tz=None)),
+ TimeResolution.Hours,
+ )
+
+
+class TestTimeParserFunctions(unittest.TestCase):
+ def test_parse_time(self):
+ testtuples = (
+ ("01:23:45", {"hh": "01", "mm": "23", "ss": "45", "tz": None}),
+ ("24:00:00", {"hh": "24", "mm": "00", "ss": "00", "tz": None}),
+ (
+ "23:21:28,512400",
+ {"hh": "23", "mm": "21", "ss": "28.512400", "tz": None},
+ ),
+ (
+ "23:21:28.512400",
+ {"hh": "23", "mm": "21", "ss": "28.512400", "tz": None},
+ ),
+ (
+ "01:03:11.858714",
+ {"hh": "01", "mm": "03", "ss": "11.858714", "tz": None},
+ ),
+ (
+ "14:43:59.9999997",
+ {"hh": "14", "mm": "43", "ss": "59.9999997", "tz": None},
+ ),
+ ("01:23", {"hh": "01", "mm": "23", "ss": None, "tz": None}),
+ ("24:00", {"hh": "24", "mm": "00", "ss": None, "tz": None}),
+ ("01:23,4567", {"hh": "01", "mm": "23.4567", "ss": None, "tz": None}),
+ ("01:23.4567", {"hh": "01", "mm": "23.4567", "ss": None, "tz": None}),
+ ("012345", {"hh": "01", "mm": "23", "ss": "45", "tz": None}),
+ ("240000", {"hh": "24", "mm": "00", "ss": "00", "tz": None}),
+ ("232128,512400", {"hh": "23", "mm": "21", "ss": "28.512400", "tz": None}),
+ ("232128.512400", {"hh": "23", "mm": "21", "ss": "28.512400", "tz": None}),
+ ("010311.858714", {"hh": "01", "mm": "03", "ss": "11.858714", "tz": None}),
+ (
+ "144359.9999997",
+ {"hh": "14", "mm": "43", "ss": "59.9999997", "tz": None},
+ ),
+ ("0123", {"hh": "01", "mm": "23", "ss": None, "tz": None}),
+ ("2400", {"hh": "24", "mm": "00", "ss": None, "tz": None}),
+ ("01", {"hh": "01", "mm": None, "ss": None, "tz": None}),
+ ("24", {"hh": "24", "mm": None, "ss": None, "tz": None}),
+ ("12,5", {"hh": "12.5", "mm": None, "ss": None, "tz": None}),
+ ("12.5", {"hh": "12.5", "mm": None, "ss": None, "tz": None}),
+ (
+ "232128,512400+00:00",
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "232128.512400+00:00",
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "0123,4567+00:00",
+ {
+ "hh": "01",
+ "mm": "23.4567",
+ "ss": None,
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "0123.4567+00:00",
+ {
+ "hh": "01",
+ "mm": "23.4567",
+ "ss": None,
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "01,4567+00:00",
+ {
+ "hh": "01.4567",
+ "mm": None,
+ "ss": None,
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "01.4567+00:00",
+ {
+ "hh": "01.4567",
+ "mm": None,
+ "ss": None,
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "01:23:45+00:00",
+ {
+ "hh": "01",
+ "mm": "23",
+ "ss": "45",
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "24:00:00+00:00",
+ {
+ "hh": "24",
+ "mm": "00",
+ "ss": "00",
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "23:21:28.512400+00:00",
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "01:23+00:00",
+ {
+ "hh": "01",
+ "mm": "23",
+ "ss": None,
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "24:00+00:00",
+ {
+ "hh": "24",
+ "mm": "00",
+ "ss": None,
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "01:23.4567+00:00",
+ {
+ "hh": "01",
+ "mm": "23.4567",
+ "ss": None,
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ },
+ ),
+ (
+ "23:21:28.512400+11:15",
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "11", "15", "+11:15"),
+ },
+ ),
+ (
+ "23:21:28.512400-12:34",
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(True, None, "12", "34", "-12:34"),
+ },
+ ),
+ (
+ "23:21:28.512400Z",
+ {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, True, None, None, "Z"),
+ },
+ ),
+ (
+ "06:14:00.000123Z",
+ {
+ "hh": "06",
+ "mm": "14",
+ "ss": "00.000123",
+ "tz": TimezoneTuple(False, True, None, None, "Z"),
+ },
+ ),
+ )
+
+ for testtuple in testtuples:
+ with mock.patch.object(
+ aniso8601.time.PythonTimeBuilder, "build_time"
+ ) as mockBuildTime:
+
+ mockBuildTime.return_value = testtuple[1]
+
+ result = parse_time(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+ mockBuildTime.assert_called_once_with(**testtuple[1])
+
+ def test_parse_time_badtype(self):
+ testtuples = (None, 1, False, 1.234)
+
+ for testtuple in testtuples:
+ with self.assertRaises(ValueError):
+ parse_time(testtuple, builder=None)
+
+ def test_parse_time_badstr(self):
+ testtuples = (
+ "A6:14:00.000123Z",
+ "06:14:0B",
+ "06:1 :02",
+ "0000,70:24,9",
+ "00.27:5332",
+ "bad",
+ "",
+ )
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_time(testtuple, builder=None)
+
+ def test_parse_time_mockbuilder(self):
+ mockBuilder = mock.Mock()
+
+ expectedargs = {"hh": "01", "mm": "23", "ss": "45", "tz": None}
+
+ mockBuilder.build_time.return_value = expectedargs
+
+ result = parse_time("01:23:45", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_time.assert_called_once_with(**expectedargs)
+
+ mockBuilder = mock.Mock()
+
+ expectedargs = {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "00", "00", "+00:00"),
+ }
+
+ mockBuilder.build_time.return_value = expectedargs
+
+ result = parse_time("232128.512400+00:00", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_time.assert_called_once_with(**expectedargs)
+
+ mockBuilder = mock.Mock()
+
+ expectedargs = {
+ "hh": "23",
+ "mm": "21",
+ "ss": "28.512400",
+ "tz": TimezoneTuple(False, None, "11", "15", "+11:15"),
+ }
+
+ mockBuilder.build_time.return_value = expectedargs
+
+ result = parse_time("23:21:28.512400+11:15", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_time.assert_called_once_with(**expectedargs)
+
+ def test_parse_datetime(self):
+ testtuples = (
+ (
+ "2019-06-05T01:03:11,858714",
+ (
+ DateTuple("2019", "06", "05", None, None, None),
+ TimeTuple("01", "03", "11.858714", None),
+ ),
+ ),
+ (
+ "2019-06-05T01:03:11.858714",
+ (
+ DateTuple("2019", "06", "05", None, None, None),
+ TimeTuple("01", "03", "11.858714", None),
+ ),
+ ),
+ (
+ "1981-04-05T23:21:28.512400Z",
+ (
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple(
+ "23",
+ "21",
+ "28.512400",
+ TimezoneTuple(False, True, None, None, "Z"),
+ ),
+ ),
+ ),
+ (
+ "1981095T23:21:28.512400-12:34",
+ (
+ DateTuple("1981", None, None, None, None, "095"),
+ TimeTuple(
+ "23",
+ "21",
+ "28.512400",
+ TimezoneTuple(True, None, "12", "34", "-12:34"),
+ ),
+ ),
+ ),
+ (
+ "19810405T23:21:28+00",
+ (
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple(
+ "23", "21", "28", TimezoneTuple(False, None, "00", None, "+00")
+ ),
+ ),
+ ),
+ (
+ "19810405T23:21:28+00:00",
+ (
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple(
+ "23",
+ "21",
+ "28",
+ TimezoneTuple(False, None, "00", "00", "+00:00"),
+ ),
+ ),
+ ),
+ )
+
+ for testtuple in testtuples:
+ with mock.patch.object(
+ aniso8601.time.PythonTimeBuilder, "build_datetime"
+ ) as mockBuildDateTime:
+
+ mockBuildDateTime.return_value = testtuple[1]
+
+ result = parse_datetime(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+ mockBuildDateTime.assert_called_once_with(*testtuple[1])
+
+ def test_parse_datetime_spacedelimited(self):
+ expectedargs = (
+ DateTuple("2004", None, None, "53", "6", None),
+ TimeTuple(
+ "23", "21", "28.512400", TimezoneTuple(True, None, "12", "34", "-12:34")
+ ),
+ )
+
+ with mock.patch.object(
+ aniso8601.time.PythonTimeBuilder, "build_datetime"
+ ) as mockBuildDateTime:
+
+ mockBuildDateTime.return_value = expectedargs
+
+ result = parse_datetime("2004-W53-6 23:21:28.512400-12:34", delimiter=" ")
+
+ self.assertEqual(result, expectedargs)
+ mockBuildDateTime.assert_called_once_with(*expectedargs)
+
+ def test_parse_datetime_commadelimited(self):
+ expectedargs = (
+ DateTuple("1981", "04", "05", None, None, None),
+ TimeTuple(
+ "23", "21", "28.512400", TimezoneTuple(False, True, None, None, "Z")
+ ),
+ )
+
+ with mock.patch.object(
+ aniso8601.time.PythonTimeBuilder, "build_datetime"
+ ) as mockBuildDateTime:
+
+ mockBuildDateTime.return_value = expectedargs
+
+ result = parse_datetime("1981-04-05,23:21:28,512400Z", delimiter=",")
+
+ self.assertEqual(result, expectedargs)
+ mockBuildDateTime.assert_called_once_with(*expectedargs)
+
+ def test_parse_datetime_baddelimiter(self):
+ testtuples = (
+ "1981-04-05,23:21:28,512400Z",
+ "2004-W53-6 23:21:28.512400-12:3",
+ "1981040523:21:28",
+ )
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_datetime(testtuple, builder=None)
+
+ def test_parse_datetime_badtype(self):
+ testtuples = (None, 1, False, 1.234)
+
+ for testtuple in testtuples:
+ with self.assertRaises(ValueError):
+ parse_datetime(testtuple, builder=None)
+
+ def test_parse_datetime_badstr(self):
+ testtuples = (
+ "1981-04-05TA6:14:00.000123Z",
+ "2004-W53-6T06:14:0B",
+ "2014-01-230T23:21:28+00",
+ "201401230T01:03:11.858714",
+ "9999 W53T49",
+ "9T0000,70:24,9",
+ "bad",
+ "",
+ )
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_datetime(testtuple, builder=None)
+
+ def test_parse_datetime_mockbuilder(self):
+ mockBuilder = mock.Mock()
+
+ expectedargs = (
+ DateTuple("1981", None, None, None, None, "095"),
+ TimeTuple(
+ "23", "21", "28.512400", TimezoneTuple(True, None, "12", "34", "-12:34")
+ ),
+ )
+
+ mockBuilder.build_datetime.return_value = expectedargs
+
+ result = parse_datetime("1981095T23:21:28.512400-12:34", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_datetime.assert_called_once_with(*expectedargs)
diff --git a/libs/aniso8601/tests/test_timezone.py b/libs/aniso8601/tests/test_timezone.py
new file mode 100644
index 000000000..5df9671f1
--- /dev/null
+++ b/libs/aniso8601/tests/test_timezone.py
@@ -0,0 +1,123 @@
+# -*- 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.
+
+import unittest
+
+import aniso8601
+from aniso8601.exceptions import ISOFormatError
+from aniso8601.tests.compat import mock
+from aniso8601.timezone import parse_timezone
+
+
+class TestTimezoneParserFunctions(unittest.TestCase):
+ def test_parse_timezone(self):
+ testtuples = (
+ ("Z", {"negative": False, "Z": True, "name": "Z"}),
+ ("+00:00", {"negative": False, "hh": "00", "mm": "00", "name": "+00:00"}),
+ ("+01:00", {"negative": False, "hh": "01", "mm": "00", "name": "+01:00"}),
+ ("-01:00", {"negative": True, "hh": "01", "mm": "00", "name": "-01:00"}),
+ ("+00:12", {"negative": False, "hh": "00", "mm": "12", "name": "+00:12"}),
+ ("+01:23", {"negative": False, "hh": "01", "mm": "23", "name": "+01:23"}),
+ ("-01:23", {"negative": True, "hh": "01", "mm": "23", "name": "-01:23"}),
+ ("+0000", {"negative": False, "hh": "00", "mm": "00", "name": "+0000"}),
+ ("+0100", {"negative": False, "hh": "01", "mm": "00", "name": "+0100"}),
+ ("-0100", {"negative": True, "hh": "01", "mm": "00", "name": "-0100"}),
+ ("+0012", {"negative": False, "hh": "00", "mm": "12", "name": "+0012"}),
+ ("+0123", {"negative": False, "hh": "01", "mm": "23", "name": "+0123"}),
+ ("-0123", {"negative": True, "hh": "01", "mm": "23", "name": "-0123"}),
+ ("+00", {"negative": False, "hh": "00", "mm": None, "name": "+00"}),
+ ("+01", {"negative": False, "hh": "01", "mm": None, "name": "+01"}),
+ ("-01", {"negative": True, "hh": "01", "mm": None, "name": "-01"}),
+ ("+12", {"negative": False, "hh": "12", "mm": None, "name": "+12"}),
+ ("-12", {"negative": True, "hh": "12", "mm": None, "name": "-12"}),
+ )
+
+ for testtuple in testtuples:
+ with mock.patch.object(
+ aniso8601.timezone.PythonTimeBuilder, "build_timezone"
+ ) as mockBuildTimezone:
+
+ mockBuildTimezone.return_value = testtuple[1]
+
+ result = parse_timezone(testtuple[0])
+
+ self.assertEqual(result, testtuple[1])
+ mockBuildTimezone.assert_called_once_with(**testtuple[1])
+
+ def test_parse_timezone_badtype(self):
+ testtuples = (None, 1, False, 1.234)
+
+ for testtuple in testtuples:
+ with self.assertRaises(ValueError):
+ parse_timezone(testtuple, builder=None)
+
+ def test_parse_timezone_badstr(self):
+ testtuples = (
+ "+1",
+ "-00",
+ "-0000",
+ "-00:00",
+ "01",
+ "0123",
+ "@12:34",
+ "Y",
+ " Z",
+ "Z ",
+ " Z ",
+ "bad",
+ "",
+ )
+
+ for testtuple in testtuples:
+ with self.assertRaises(ISOFormatError):
+ parse_timezone(testtuple, builder=None)
+
+ def test_parse_timezone_mockbuilder(self):
+ mockBuilder = mock.Mock()
+
+ expectedargs = {"negative": False, "Z": True, "name": "Z"}
+
+ mockBuilder.build_timezone.return_value = expectedargs
+
+ result = parse_timezone("Z", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_timezone.assert_called_once_with(**expectedargs)
+
+ mockBuilder = mock.Mock()
+
+ expectedargs = {"negative": False, "hh": "00", "mm": "00", "name": "+00:00"}
+
+ mockBuilder.build_timezone.return_value = expectedargs
+
+ result = parse_timezone("+00:00", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_timezone.assert_called_once_with(**expectedargs)
+
+ mockBuilder = mock.Mock()
+
+ expectedargs = {"negative": True, "hh": "01", "mm": "23", "name": "-01:23"}
+
+ mockBuilder.build_timezone.return_value = expectedargs
+
+ result = parse_timezone("-01:23", builder=mockBuilder)
+
+ self.assertEqual(result, expectedargs)
+ mockBuilder.build_timezone.assert_called_once_with(**expectedargs)
+
+ def test_parse_timezone_negativezero(self):
+ # A 0 offset cannot be negative
+ with self.assertRaises(ISOFormatError):
+ parse_timezone("-00:00", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_timezone("-0000", builder=None)
+
+ with self.assertRaises(ISOFormatError):
+ parse_timezone("-00", builder=None)
diff --git a/libs/aniso8601/tests/test_utcoffset.py b/libs/aniso8601/tests/test_utcoffset.py
new file mode 100644
index 000000000..11fa4f7d9
--- /dev/null
+++ b/libs/aniso8601/tests/test_utcoffset.py
@@ -0,0 +1,56 @@
+# -*- 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.
+
+import datetime
+import pickle
+import unittest
+
+from aniso8601.utcoffset import UTCOffset
+
+
+class TestUTCOffset(unittest.TestCase):
+ def test_pickle(self):
+ # Make sure timezone objects are pickleable
+ testutcoffset = UTCOffset(name="UTC", minutes=0)
+
+ utcoffsetpickle = pickle.dumps(testutcoffset)
+
+ resultutcoffset = pickle.loads(utcoffsetpickle)
+
+ self.assertEqual(resultutcoffset._name, testutcoffset._name)
+ self.assertEqual(resultutcoffset._utcdelta, testutcoffset._utcdelta)
+
+ def test_repr(self):
+ self.assertEqual(str(UTCOffset(minutes=0)), "+0:00:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=60)), "+1:00:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=-60)), "-1:00:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=12)), "+0:12:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=-12)), "-0:12:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=83)), "+1:23:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=-83)), "-1:23:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=1440)), "+1 day, 0:00:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=-1440)), "-1 day, 0:00:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=2967)), "+2 days, 1:27:00 UTC")
+
+ self.assertEqual(str(UTCOffset(minutes=-2967)), "-2 days, 1:27:00 UTC")
+
+ def test_dst(self):
+ tzinfoobject = UTCOffset(minutes=240)
+ # This would raise ISOFormatError or a TypeError if dst info is invalid
+ result = datetime.datetime.now(tzinfoobject)
+ # Hacky way to make sure the tzinfo is what we'd expect
+ self.assertEqual(result.tzinfo.utcoffset(None), datetime.timedelta(hours=4))
diff --git a/libs/aniso8601/time.py b/libs/aniso8601/time.py
new file mode 100644
index 000000000..31fab048e
--- /dev/null
+++ b/libs/aniso8601/time.py
@@ -0,0 +1,203 @@
+# -*- 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 TupleBuilder
+from aniso8601.builders.python import PythonTimeBuilder
+from aniso8601.compat import is_string
+from aniso8601.date import parse_date
+from aniso8601.decimalfraction import normalize
+from aniso8601.exceptions import ISOFormatError
+from aniso8601.resolution import TimeResolution
+from aniso8601.timezone import parse_timezone
+
+TIMEZONE_DELIMITERS = ["Z", "+", "-"]
+
+
+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
+ isotimetuple = parse_time(isotimestr, builder=TupleBuilder)
+
+ return _get_time_resolution(isotimetuple)
+
+
+def get_datetime_resolution(isodatetimestr, delimiter="T"):
+ # <date>T<time>
+ #
+ # Time part cannot be omittted so return time resolution
+ isotimetuple = parse_datetime(
+ isodatetimestr, delimiter=delimiter, builder=TupleBuilder
+ ).time
+
+ return _get_time_resolution(isotimetuple)
+
+
+def _get_time_resolution(isotimetuple):
+ if isotimetuple.ss is not None:
+ return TimeResolution.Seconds
+
+ if isotimetuple.mm is not None:
+ return TimeResolution.Minutes
+
+ return TimeResolution.Hours
+
+
+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_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)
diff --git a/libs/aniso8601/timezone.py b/libs/aniso8601/timezone.py
new file mode 100644
index 000000000..3b66105bd
--- /dev/null
+++ b/libs/aniso8601/timezone.py
@@ -0,0 +1,62 @@
+# -*- 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.python import PythonTimeBuilder
+from aniso8601.compat import is_string
+from aniso8601.exceptions import ISOFormatError
+
+
+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))
diff --git a/libs/aniso8601/utcoffset.py b/libs/aniso8601/utcoffset.py
new file mode 100644
index 000000000..11872a01e
--- /dev/null
+++ b/libs/aniso8601/utcoffset.py
@@ -0,0 +1,71 @@
+# -*- 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.
+
+import datetime
+
+
+class UTCOffset(datetime.tzinfo):
+ def __init__(self, name=None, minutes=None):
+ # We build an offset in this manner since the
+ # tzinfo class must have an init
+ # "method that can be called with no arguments"
+ self._name = name
+
+ if minutes is not None:
+ self._utcdelta = datetime.timedelta(minutes=minutes)
+ else:
+ self._utcdelta = None
+
+ def __repr__(self):
+ if self._utcdelta >= datetime.timedelta(hours=0):
+ return "+{0} UTC".format(self._utcdelta)
+
+ # From the docs:
+ # String representations of timedelta objects are normalized
+ # similarly to their internal representation. This leads to
+ # somewhat unusual results for negative timedeltas.
+
+ # Clean this up for printing purposes
+ # Negative deltas start at -1 day
+ correcteddays = abs(self._utcdelta.days + 1)
+
+ # Negative deltas have a positive seconds
+ deltaseconds = (24 * 60 * 60) - self._utcdelta.seconds
+
+ # (24 hours / day) * (60 minutes / hour) * (60 seconds / hour)
+ days, remainder = divmod(deltaseconds, 24 * 60 * 60)
+
+ # (1 hour) * (60 minutes / hour) * (60 seconds / hour)
+ hours, remainder = divmod(remainder, 1 * 60 * 60)
+
+ # (1 minute) * (60 seconds / minute)
+ minutes, seconds = divmod(remainder, 1 * 60)
+
+ # Add any remaining days to the correcteddays count
+ correcteddays += days
+
+ if correcteddays == 0:
+ return "-{0}:{1:02}:{2:02} UTC".format(hours, minutes, seconds)
+ elif correcteddays == 1:
+ return "-1 day, {0}:{1:02}:{2:02} UTC".format(hours, minutes, seconds)
+
+ return "-{0} days, {1}:{2:02}:{3:02} UTC".format(
+ correcteddays, hours, minutes, seconds
+ )
+
+ def utcoffset(self, dt):
+ return self._utcdelta
+
+ def tzname(self, dt):
+ return self._name
+
+ def dst(self, dt):
+ # ISO 8601 specifies offsets should be different if DST is required,
+ # instead of allowing for a DST to be specified
+ # https://docs.python.org/2/library/datetime.html#datetime.tzinfo.dst
+ return datetime.timedelta(0)