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