python.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2021, Brandon Nielsen
  3. # All rights reserved.
  4. #
  5. # This software may be modified and distributed under the terms
  6. # of the BSD license. See the LICENSE file for details.
  7. import datetime
  8. from collections import namedtuple
  9. from functools import partial
  10. from aniso8601.builders import (
  11. BaseTimeBuilder,
  12. DatetimeTuple,
  13. DateTuple,
  14. Limit,
  15. TimeTuple,
  16. TupleBuilder,
  17. cast,
  18. range_check,
  19. )
  20. from aniso8601.exceptions import (
  21. DayOutOfBoundsError,
  22. HoursOutOfBoundsError,
  23. ISOFormatError,
  24. LeapSecondError,
  25. MidnightBoundsError,
  26. MinutesOutOfBoundsError,
  27. MonthOutOfBoundsError,
  28. SecondsOutOfBoundsError,
  29. WeekOutOfBoundsError,
  30. YearOutOfBoundsError,
  31. )
  32. from aniso8601.utcoffset import UTCOffset
  33. DAYS_PER_YEAR = 365
  34. DAYS_PER_MONTH = 30
  35. DAYS_PER_WEEK = 7
  36. HOURS_PER_DAY = 24
  37. MINUTES_PER_HOUR = 60
  38. MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY
  39. SECONDS_PER_MINUTE = 60
  40. SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE
  41. MICROSECONDS_PER_SECOND = int(1e6)
  42. MICROSECONDS_PER_MINUTE = 60 * MICROSECONDS_PER_SECOND
  43. MICROSECONDS_PER_HOUR = 60 * MICROSECONDS_PER_MINUTE
  44. MICROSECONDS_PER_DAY = 24 * MICROSECONDS_PER_HOUR
  45. MICROSECONDS_PER_WEEK = 7 * MICROSECONDS_PER_DAY
  46. MICROSECONDS_PER_MONTH = DAYS_PER_MONTH * MICROSECONDS_PER_DAY
  47. MICROSECONDS_PER_YEAR = DAYS_PER_YEAR * MICROSECONDS_PER_DAY
  48. TIMEDELTA_MAX_DAYS = datetime.timedelta.max.days
  49. FractionalComponent = namedtuple(
  50. "FractionalComponent", ["principal", "microsecondremainder"]
  51. )
  52. def year_range_check(valuestr, limit):
  53. YYYYstr = valuestr
  54. # Truncated dates, like '19', refer to 1900-1999 inclusive,
  55. # we simply parse to 1900
  56. if len(valuestr) < 4:
  57. # Shift 0s in from the left to form complete year
  58. YYYYstr = valuestr.ljust(4, "0")
  59. return range_check(YYYYstr, limit)
  60. def fractional_range_check(conversion, valuestr, limit):
  61. if valuestr is None:
  62. return None
  63. if "." in valuestr:
  64. castfunc = partial(_cast_to_fractional_component, conversion)
  65. else:
  66. castfunc = int
  67. value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring)
  68. if type(value) is FractionalComponent:
  69. tocheck = float(valuestr)
  70. else:
  71. tocheck = int(valuestr)
  72. if limit.min is not None and tocheck < limit.min:
  73. raise limit.rangeexception(limit.rangeerrorstring)
  74. if limit.max is not None and tocheck > limit.max:
  75. raise limit.rangeexception(limit.rangeerrorstring)
  76. return value
  77. def _cast_to_fractional_component(conversion, floatstr):
  78. # Splits a string with a decimal point into an int, and
  79. # int representing the floating point remainder as a number
  80. # of microseconds, determined by multiplying by conversion
  81. intpart, floatpart = floatstr.split(".")
  82. intvalue = int(intpart)
  83. preconvertedvalue = int(floatpart)
  84. convertedvalue = (preconvertedvalue * conversion) // (10 ** len(floatpart))
  85. return FractionalComponent(intvalue, convertedvalue)
  86. class PythonTimeBuilder(BaseTimeBuilder):
  87. # 0000 (1 BC) is not representable as a Python date
  88. DATE_YYYY_LIMIT = Limit(
  89. "Invalid year string.",
  90. datetime.MINYEAR,
  91. datetime.MAXYEAR,
  92. YearOutOfBoundsError,
  93. "Year must be between {0}..{1}.".format(datetime.MINYEAR, datetime.MAXYEAR),
  94. year_range_check,
  95. )
  96. TIME_HH_LIMIT = Limit(
  97. "Invalid hour string.",
  98. 0,
  99. 24,
  100. HoursOutOfBoundsError,
  101. "Hour must be between 0..24 with " "24 representing midnight.",
  102. partial(fractional_range_check, MICROSECONDS_PER_HOUR),
  103. )
  104. TIME_MM_LIMIT = Limit(
  105. "Invalid minute string.",
  106. 0,
  107. 59,
  108. MinutesOutOfBoundsError,
  109. "Minute must be between 0..59.",
  110. partial(fractional_range_check, MICROSECONDS_PER_MINUTE),
  111. )
  112. TIME_SS_LIMIT = Limit(
  113. "Invalid second string.",
  114. 0,
  115. 60,
  116. SecondsOutOfBoundsError,
  117. "Second must be between 0..60 with " "60 representing a leap second.",
  118. partial(fractional_range_check, MICROSECONDS_PER_SECOND),
  119. )
  120. DURATION_PNY_LIMIT = Limit(
  121. "Invalid year duration string.",
  122. None,
  123. None,
  124. YearOutOfBoundsError,
  125. None,
  126. partial(fractional_range_check, MICROSECONDS_PER_YEAR),
  127. )
  128. DURATION_PNM_LIMIT = Limit(
  129. "Invalid month duration string.",
  130. None,
  131. None,
  132. MonthOutOfBoundsError,
  133. None,
  134. partial(fractional_range_check, MICROSECONDS_PER_MONTH),
  135. )
  136. DURATION_PNW_LIMIT = Limit(
  137. "Invalid week duration string.",
  138. None,
  139. None,
  140. WeekOutOfBoundsError,
  141. None,
  142. partial(fractional_range_check, MICROSECONDS_PER_WEEK),
  143. )
  144. DURATION_PND_LIMIT = Limit(
  145. "Invalid day duration string.",
  146. None,
  147. None,
  148. DayOutOfBoundsError,
  149. None,
  150. partial(fractional_range_check, MICROSECONDS_PER_DAY),
  151. )
  152. DURATION_TNH_LIMIT = Limit(
  153. "Invalid hour duration string.",
  154. None,
  155. None,
  156. HoursOutOfBoundsError,
  157. None,
  158. partial(fractional_range_check, MICROSECONDS_PER_HOUR),
  159. )
  160. DURATION_TNM_LIMIT = Limit(
  161. "Invalid minute duration string.",
  162. None,
  163. None,
  164. MinutesOutOfBoundsError,
  165. None,
  166. partial(fractional_range_check, MICROSECONDS_PER_MINUTE),
  167. )
  168. DURATION_TNS_LIMIT = Limit(
  169. "Invalid second duration string.",
  170. None,
  171. None,
  172. SecondsOutOfBoundsError,
  173. None,
  174. partial(fractional_range_check, MICROSECONDS_PER_SECOND),
  175. )
  176. DATE_RANGE_DICT = BaseTimeBuilder.DATE_RANGE_DICT
  177. DATE_RANGE_DICT["YYYY"] = DATE_YYYY_LIMIT
  178. TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT}
  179. DURATION_RANGE_DICT = {
  180. "PnY": DURATION_PNY_LIMIT,
  181. "PnM": DURATION_PNM_LIMIT,
  182. "PnW": DURATION_PNW_LIMIT,
  183. "PnD": DURATION_PND_LIMIT,
  184. "TnH": DURATION_TNH_LIMIT,
  185. "TnM": DURATION_TNM_LIMIT,
  186. "TnS": DURATION_TNS_LIMIT,
  187. }
  188. @classmethod
  189. def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None):
  190. YYYY, MM, DD, Www, D, DDD = cls.range_check_date(YYYY, MM, DD, Www, D, DDD)
  191. if MM is None:
  192. MM = 1
  193. if DD is None:
  194. DD = 1
  195. if DDD is not None:
  196. return PythonTimeBuilder._build_ordinal_date(YYYY, DDD)
  197. if Www is not None:
  198. return PythonTimeBuilder._build_week_date(YYYY, Www, isoday=D)
  199. return datetime.date(YYYY, MM, DD)
  200. @classmethod
  201. def build_time(cls, hh=None, mm=None, ss=None, tz=None):
  202. # Builds a time from the given parts, handling fractional arguments
  203. # where necessary
  204. hours = 0
  205. minutes = 0
  206. seconds = 0
  207. microseconds = 0
  208. hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz)
  209. if type(hh) is FractionalComponent:
  210. hours = hh.principal
  211. microseconds = hh.microsecondremainder
  212. elif hh is not None:
  213. hours = hh
  214. if type(mm) is FractionalComponent:
  215. minutes = mm.principal
  216. microseconds = mm.microsecondremainder
  217. elif mm is not None:
  218. minutes = mm
  219. if type(ss) is FractionalComponent:
  220. seconds = ss.principal
  221. microseconds = ss.microsecondremainder
  222. elif ss is not None:
  223. seconds = ss
  224. (
  225. hours,
  226. minutes,
  227. seconds,
  228. microseconds,
  229. ) = PythonTimeBuilder._distribute_microseconds(
  230. microseconds,
  231. (hours, minutes, seconds),
  232. (MICROSECONDS_PER_HOUR, MICROSECONDS_PER_MINUTE, MICROSECONDS_PER_SECOND),
  233. )
  234. # Move midnight into range
  235. if hours == 24:
  236. hours = 0
  237. # Datetimes don't handle fractional components, so we use a timedelta
  238. if tz is not None:
  239. return (
  240. datetime.datetime(
  241. 1, 1, 1, hour=hours, minute=minutes, tzinfo=cls._build_object(tz)
  242. )
  243. + datetime.timedelta(seconds=seconds, microseconds=microseconds)
  244. ).timetz()
  245. return (
  246. datetime.datetime(1, 1, 1, hour=hours, minute=minutes)
  247. + datetime.timedelta(seconds=seconds, microseconds=microseconds)
  248. ).time()
  249. @classmethod
  250. def build_datetime(cls, date, time):
  251. return datetime.datetime.combine(
  252. cls._build_object(date), cls._build_object(time)
  253. )
  254. @classmethod
  255. def build_duration(
  256. cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
  257. ):
  258. # PnY and PnM will be distributed to PnD, microsecond remainder to TnS
  259. PnY, PnM, PnW, PnD, TnH, TnM, TnS = cls.range_check_duration(
  260. PnY, PnM, PnW, PnD, TnH, TnM, TnS
  261. )
  262. seconds = TnS.principal
  263. microseconds = TnS.microsecondremainder
  264. return datetime.timedelta(
  265. days=PnD,
  266. seconds=seconds,
  267. microseconds=microseconds,
  268. minutes=TnM,
  269. hours=TnH,
  270. weeks=PnW,
  271. )
  272. @classmethod
  273. def build_interval(cls, start=None, end=None, duration=None):
  274. start, end, duration = cls.range_check_interval(start, end, duration)
  275. if start is not None and end is not None:
  276. # <start>/<end>
  277. startobject = cls._build_object(start)
  278. endobject = cls._build_object(end)
  279. return (startobject, endobject)
  280. durationobject = cls._build_object(duration)
  281. # Determine if datetime promotion is required
  282. datetimerequired = (
  283. duration.TnH is not None
  284. or duration.TnM is not None
  285. or duration.TnS is not None
  286. or durationobject.seconds != 0
  287. or durationobject.microseconds != 0
  288. )
  289. if end is not None:
  290. # <duration>/<end>
  291. endobject = cls._build_object(end)
  292. # Range check
  293. if type(end) is DateTuple and datetimerequired is True:
  294. # <end> is a date, and <duration> requires datetime resolution
  295. return (
  296. endobject,
  297. cls.build_datetime(end, TupleBuilder.build_time()) - durationobject,
  298. )
  299. return (endobject, endobject - durationobject)
  300. # <start>/<duration>
  301. startobject = cls._build_object(start)
  302. # Range check
  303. if type(start) is DateTuple and datetimerequired is True:
  304. # <start> is a date, and <duration> requires datetime resolution
  305. return (
  306. startobject,
  307. cls.build_datetime(start, TupleBuilder.build_time()) + durationobject,
  308. )
  309. return (startobject, startobject + durationobject)
  310. @classmethod
  311. def build_repeating_interval(cls, R=None, Rnn=None, interval=None):
  312. startobject = None
  313. endobject = None
  314. R, Rnn, interval = cls.range_check_repeating_interval(R, Rnn, interval)
  315. if interval.start is not None:
  316. startobject = cls._build_object(interval.start)
  317. if interval.end is not None:
  318. endobject = cls._build_object(interval.end)
  319. if interval.duration is not None:
  320. durationobject = cls._build_object(interval.duration)
  321. else:
  322. durationobject = endobject - startobject
  323. if R is True:
  324. if startobject is not None:
  325. return cls._date_generator_unbounded(startobject, durationobject)
  326. return cls._date_generator_unbounded(endobject, -durationobject)
  327. iterations = int(Rnn)
  328. if startobject is not None:
  329. return cls._date_generator(startobject, durationobject, iterations)
  330. return cls._date_generator(endobject, -durationobject, iterations)
  331. @classmethod
  332. def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""):
  333. negative, Z, hh, mm, name = cls.range_check_timezone(negative, Z, hh, mm, name)
  334. if Z is True:
  335. # Z -> UTC
  336. return UTCOffset(name="UTC", minutes=0)
  337. tzhour = int(hh)
  338. if mm is not None:
  339. tzminute = int(mm)
  340. else:
  341. tzminute = 0
  342. if negative is True:
  343. return UTCOffset(name=name, minutes=-(tzhour * 60 + tzminute))
  344. return UTCOffset(name=name, minutes=tzhour * 60 + tzminute)
  345. @classmethod
  346. def range_check_duration(
  347. cls,
  348. PnY=None,
  349. PnM=None,
  350. PnW=None,
  351. PnD=None,
  352. TnH=None,
  353. TnM=None,
  354. TnS=None,
  355. rangedict=None,
  356. ):
  357. years = 0
  358. months = 0
  359. days = 0
  360. weeks = 0
  361. hours = 0
  362. minutes = 0
  363. seconds = 0
  364. microseconds = 0
  365. PnY, PnM, PnW, PnD, TnH, TnM, TnS = BaseTimeBuilder.range_check_duration(
  366. PnY, PnM, PnW, PnD, TnH, TnM, TnS, rangedict=cls.DURATION_RANGE_DICT
  367. )
  368. if PnY is not None:
  369. if type(PnY) is FractionalComponent:
  370. years = PnY.principal
  371. microseconds = PnY.microsecondremainder
  372. else:
  373. years = PnY
  374. if years * DAYS_PER_YEAR > TIMEDELTA_MAX_DAYS:
  375. raise YearOutOfBoundsError("Duration exceeds maximum timedelta size.")
  376. if PnM is not None:
  377. if type(PnM) is FractionalComponent:
  378. months = PnM.principal
  379. microseconds = PnM.microsecondremainder
  380. else:
  381. months = PnM
  382. if months * DAYS_PER_MONTH > TIMEDELTA_MAX_DAYS:
  383. raise MonthOutOfBoundsError("Duration exceeds maximum timedelta size.")
  384. if PnW is not None:
  385. if type(PnW) is FractionalComponent:
  386. weeks = PnW.principal
  387. microseconds = PnW.microsecondremainder
  388. else:
  389. weeks = PnW
  390. if weeks * DAYS_PER_WEEK > TIMEDELTA_MAX_DAYS:
  391. raise WeekOutOfBoundsError("Duration exceeds maximum timedelta size.")
  392. if PnD is not None:
  393. if type(PnD) is FractionalComponent:
  394. days = PnD.principal
  395. microseconds = PnD.microsecondremainder
  396. else:
  397. days = PnD
  398. if days > TIMEDELTA_MAX_DAYS:
  399. raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.")
  400. if TnH is not None:
  401. if type(TnH) is FractionalComponent:
  402. hours = TnH.principal
  403. microseconds = TnH.microsecondremainder
  404. else:
  405. hours = TnH
  406. if hours // HOURS_PER_DAY > TIMEDELTA_MAX_DAYS:
  407. raise HoursOutOfBoundsError("Duration exceeds maximum timedelta size.")
  408. if TnM is not None:
  409. if type(TnM) is FractionalComponent:
  410. minutes = TnM.principal
  411. microseconds = TnM.microsecondremainder
  412. else:
  413. minutes = TnM
  414. if minutes // MINUTES_PER_DAY > TIMEDELTA_MAX_DAYS:
  415. raise MinutesOutOfBoundsError(
  416. "Duration exceeds maximum timedelta size."
  417. )
  418. if TnS is not None:
  419. if type(TnS) is FractionalComponent:
  420. seconds = TnS.principal
  421. microseconds = TnS.microsecondremainder
  422. else:
  423. seconds = TnS
  424. if seconds // SECONDS_PER_DAY > TIMEDELTA_MAX_DAYS:
  425. raise SecondsOutOfBoundsError(
  426. "Duration exceeds maximum timedelta size."
  427. )
  428. (
  429. years,
  430. months,
  431. weeks,
  432. days,
  433. hours,
  434. minutes,
  435. seconds,
  436. microseconds,
  437. ) = PythonTimeBuilder._distribute_microseconds(
  438. microseconds,
  439. (years, months, weeks, days, hours, minutes, seconds),
  440. (
  441. MICROSECONDS_PER_YEAR,
  442. MICROSECONDS_PER_MONTH,
  443. MICROSECONDS_PER_WEEK,
  444. MICROSECONDS_PER_DAY,
  445. MICROSECONDS_PER_HOUR,
  446. MICROSECONDS_PER_MINUTE,
  447. MICROSECONDS_PER_SECOND,
  448. ),
  449. )
  450. # Note that weeks can be handled without conversion to days
  451. totaldays = years * DAYS_PER_YEAR + months * DAYS_PER_MONTH + days
  452. # Check against timedelta limits
  453. if (
  454. totaldays
  455. + weeks * DAYS_PER_WEEK
  456. + hours // HOURS_PER_DAY
  457. + minutes // MINUTES_PER_DAY
  458. + seconds // SECONDS_PER_DAY
  459. > TIMEDELTA_MAX_DAYS
  460. ):
  461. raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.")
  462. return (
  463. None,
  464. None,
  465. weeks,
  466. totaldays,
  467. hours,
  468. minutes,
  469. FractionalComponent(seconds, microseconds),
  470. )
  471. @classmethod
  472. def range_check_interval(cls, start=None, end=None, duration=None):
  473. # Handles concise format, range checks any potential durations
  474. if start is not None and end is not None:
  475. # <start>/<end>
  476. # Handle concise format
  477. if cls._is_interval_end_concise(end) is True:
  478. end = cls._combine_concise_interval_tuples(start, end)
  479. return (start, end, duration)
  480. durationobject = cls._build_object(duration)
  481. if end is not None:
  482. # <duration>/<end>
  483. endobject = cls._build_object(end)
  484. # Range check
  485. if type(end) is DateTuple:
  486. enddatetime = cls.build_datetime(end, TupleBuilder.build_time())
  487. if enddatetime - datetime.datetime.min < durationobject:
  488. raise YearOutOfBoundsError("Interval end less than minimium date.")
  489. else:
  490. mindatetime = datetime.datetime.min
  491. if end.time.tz is not None:
  492. mindatetime = mindatetime.replace(tzinfo=endobject.tzinfo)
  493. if endobject - mindatetime < durationobject:
  494. raise YearOutOfBoundsError("Interval end less than minimium date.")
  495. else:
  496. # <start>/<duration>
  497. startobject = cls._build_object(start)
  498. # Range check
  499. if type(start) is DateTuple:
  500. startdatetime = cls.build_datetime(start, TupleBuilder.build_time())
  501. if datetime.datetime.max - startdatetime < durationobject:
  502. raise YearOutOfBoundsError(
  503. "Interval end greater than maximum date."
  504. )
  505. else:
  506. maxdatetime = datetime.datetime.max
  507. if start.time.tz is not None:
  508. maxdatetime = maxdatetime.replace(tzinfo=startobject.tzinfo)
  509. if maxdatetime - startobject < durationobject:
  510. raise YearOutOfBoundsError(
  511. "Interval end greater than maximum date."
  512. )
  513. return (start, end, duration)
  514. @staticmethod
  515. def _build_week_date(isoyear, isoweek, isoday=None):
  516. if isoday is None:
  517. return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta(
  518. weeks=isoweek - 1
  519. )
  520. return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta(
  521. weeks=isoweek - 1, days=isoday - 1
  522. )
  523. @staticmethod
  524. def _build_ordinal_date(isoyear, isoday):
  525. # Day of year to a date
  526. # https://stackoverflow.com/questions/2427555/python-question-year-and-day-of-year-to-date
  527. builtdate = datetime.date(isoyear, 1, 1) + datetime.timedelta(days=isoday - 1)
  528. return builtdate
  529. @staticmethod
  530. def _iso_year_start(isoyear):
  531. # Given an ISO year, returns the equivalent of the start of the year
  532. # on the Gregorian calendar (which is used by Python)
  533. # Stolen from:
  534. # http://stackoverflow.com/questions/304256/whats-the-best-way-to-find-the-inverse-of-datetime-isocalendar
  535. # Determine the location of the 4th of January, the first week of
  536. # the ISO year is the week containing the 4th of January
  537. # http://en.wikipedia.org/wiki/ISO_week_date
  538. fourth_jan = datetime.date(isoyear, 1, 4)
  539. # Note the conversion from ISO day (1 - 7) and Python day (0 - 6)
  540. delta = datetime.timedelta(days=fourth_jan.isoweekday() - 1)
  541. # Return the start of the year
  542. return fourth_jan - delta
  543. @staticmethod
  544. def _date_generator(startdate, timedelta, iterations):
  545. currentdate = startdate
  546. currentiteration = 0
  547. while currentiteration < iterations:
  548. yield currentdate
  549. # Update the values
  550. currentdate += timedelta
  551. currentiteration += 1
  552. @staticmethod
  553. def _date_generator_unbounded(startdate, timedelta):
  554. currentdate = startdate
  555. while True:
  556. yield currentdate
  557. # Update the value
  558. currentdate += timedelta
  559. @staticmethod
  560. def _distribute_microseconds(todistribute, recipients, reductions):
  561. # Given a number of microseconds as int, a tuple of ints length n
  562. # to distribute to, and a tuple of ints length n to divide todistribute
  563. # by (from largest to smallest), returns a tuple of length n + 1, with
  564. # todistribute divided across recipients using the reductions, with
  565. # the final remainder returned as the final tuple member
  566. results = []
  567. remainder = todistribute
  568. for index, reduction in enumerate(reductions):
  569. additional, remainder = divmod(remainder, reduction)
  570. results.append(recipients[index] + additional)
  571. # Always return the remaining microseconds
  572. results.append(remainder)
  573. return tuple(results)