You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
706 lines
22 KiB
706 lines
22 KiB
2 years ago
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
# Copyright (c) 2021, Brandon Nielsen
|
||
|
# All rights reserved.
|
||
|
#
|
||
|
# This software may be modified and distributed under the terms
|
||
|
# of the BSD license. See the LICENSE file for details.
|
||
|
|
||
|
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)
|