__init__.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  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 calendar
  8. from collections import namedtuple
  9. from aniso8601.exceptions import (
  10. DayOutOfBoundsError,
  11. HoursOutOfBoundsError,
  12. ISOFormatError,
  13. LeapSecondError,
  14. MidnightBoundsError,
  15. MinutesOutOfBoundsError,
  16. MonthOutOfBoundsError,
  17. SecondsOutOfBoundsError,
  18. WeekOutOfBoundsError,
  19. YearOutOfBoundsError,
  20. )
  21. DateTuple = namedtuple("Date", ["YYYY", "MM", "DD", "Www", "D", "DDD"])
  22. TimeTuple = namedtuple("Time", ["hh", "mm", "ss", "tz"])
  23. DatetimeTuple = namedtuple("Datetime", ["date", "time"])
  24. DurationTuple = namedtuple(
  25. "Duration", ["PnY", "PnM", "PnW", "PnD", "TnH", "TnM", "TnS"]
  26. )
  27. IntervalTuple = namedtuple("Interval", ["start", "end", "duration"])
  28. RepeatingIntervalTuple = namedtuple("RepeatingInterval", ["R", "Rnn", "interval"])
  29. TimezoneTuple = namedtuple("Timezone", ["negative", "Z", "hh", "mm", "name"])
  30. Limit = namedtuple(
  31. "Limit",
  32. [
  33. "casterrorstring",
  34. "min",
  35. "max",
  36. "rangeexception",
  37. "rangeerrorstring",
  38. "rangefunc",
  39. ],
  40. )
  41. def cast(
  42. value,
  43. castfunction,
  44. caughtexceptions=(ValueError,),
  45. thrownexception=ISOFormatError,
  46. thrownmessage=None,
  47. ):
  48. try:
  49. result = castfunction(value)
  50. except caughtexceptions:
  51. raise thrownexception(thrownmessage)
  52. return result
  53. def range_check(valuestr, limit):
  54. # Returns cast value if in range, raises defined exceptions on failure
  55. if valuestr is None:
  56. return None
  57. if "." in valuestr:
  58. castfunc = float
  59. else:
  60. castfunc = int
  61. value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring)
  62. if limit.min is not None and value < limit.min:
  63. raise limit.rangeexception(limit.rangeerrorstring)
  64. if limit.max is not None and value > limit.max:
  65. raise limit.rangeexception(limit.rangeerrorstring)
  66. return value
  67. class BaseTimeBuilder(object):
  68. # Limit tuple format cast function, cast error string,
  69. # lower limit, upper limit, limit error string
  70. DATE_YYYY_LIMIT = Limit(
  71. "Invalid year string.",
  72. 0000,
  73. 9999,
  74. YearOutOfBoundsError,
  75. "Year must be between 1..9999.",
  76. range_check,
  77. )
  78. DATE_MM_LIMIT = Limit(
  79. "Invalid month string.",
  80. 1,
  81. 12,
  82. MonthOutOfBoundsError,
  83. "Month must be between 1..12.",
  84. range_check,
  85. )
  86. DATE_DD_LIMIT = Limit(
  87. "Invalid day string.",
  88. 1,
  89. 31,
  90. DayOutOfBoundsError,
  91. "Day must be between 1..31.",
  92. range_check,
  93. )
  94. DATE_WWW_LIMIT = Limit(
  95. "Invalid week string.",
  96. 1,
  97. 53,
  98. WeekOutOfBoundsError,
  99. "Week number must be between 1..53.",
  100. range_check,
  101. )
  102. DATE_D_LIMIT = Limit(
  103. "Invalid weekday string.",
  104. 1,
  105. 7,
  106. DayOutOfBoundsError,
  107. "Weekday number must be between 1..7.",
  108. range_check,
  109. )
  110. DATE_DDD_LIMIT = Limit(
  111. "Invalid ordinal day string.",
  112. 1,
  113. 366,
  114. DayOutOfBoundsError,
  115. "Ordinal day must be between 1..366.",
  116. range_check,
  117. )
  118. TIME_HH_LIMIT = Limit(
  119. "Invalid hour string.",
  120. 0,
  121. 24,
  122. HoursOutOfBoundsError,
  123. "Hour must be between 0..24 with " "24 representing midnight.",
  124. range_check,
  125. )
  126. TIME_MM_LIMIT = Limit(
  127. "Invalid minute string.",
  128. 0,
  129. 59,
  130. MinutesOutOfBoundsError,
  131. "Minute must be between 0..59.",
  132. range_check,
  133. )
  134. TIME_SS_LIMIT = Limit(
  135. "Invalid second string.",
  136. 0,
  137. 60,
  138. SecondsOutOfBoundsError,
  139. "Second must be between 0..60 with " "60 representing a leap second.",
  140. range_check,
  141. )
  142. TZ_HH_LIMIT = Limit(
  143. "Invalid timezone hour string.",
  144. 0,
  145. 23,
  146. HoursOutOfBoundsError,
  147. "Hour must be between 0..23.",
  148. range_check,
  149. )
  150. TZ_MM_LIMIT = Limit(
  151. "Invalid timezone minute string.",
  152. 0,
  153. 59,
  154. MinutesOutOfBoundsError,
  155. "Minute must be between 0..59.",
  156. range_check,
  157. )
  158. DURATION_PNY_LIMIT = Limit(
  159. "Invalid year duration string.",
  160. 0,
  161. None,
  162. ISOFormatError,
  163. "Duration years component must be positive.",
  164. range_check,
  165. )
  166. DURATION_PNM_LIMIT = Limit(
  167. "Invalid month duration string.",
  168. 0,
  169. None,
  170. ISOFormatError,
  171. "Duration months component must be positive.",
  172. range_check,
  173. )
  174. DURATION_PNW_LIMIT = Limit(
  175. "Invalid week duration string.",
  176. 0,
  177. None,
  178. ISOFormatError,
  179. "Duration weeks component must be positive.",
  180. range_check,
  181. )
  182. DURATION_PND_LIMIT = Limit(
  183. "Invalid day duration string.",
  184. 0,
  185. None,
  186. ISOFormatError,
  187. "Duration days component must be positive.",
  188. range_check,
  189. )
  190. DURATION_TNH_LIMIT = Limit(
  191. "Invalid hour duration string.",
  192. 0,
  193. None,
  194. ISOFormatError,
  195. "Duration hours component must be positive.",
  196. range_check,
  197. )
  198. DURATION_TNM_LIMIT = Limit(
  199. "Invalid minute duration string.",
  200. 0,
  201. None,
  202. ISOFormatError,
  203. "Duration minutes component must be positive.",
  204. range_check,
  205. )
  206. DURATION_TNS_LIMIT = Limit(
  207. "Invalid second duration string.",
  208. 0,
  209. None,
  210. ISOFormatError,
  211. "Duration seconds component must be positive.",
  212. range_check,
  213. )
  214. INTERVAL_RNN_LIMIT = Limit(
  215. "Invalid duration repetition string.",
  216. 0,
  217. None,
  218. ISOFormatError,
  219. "Duration repetition count must be positive.",
  220. range_check,
  221. )
  222. DATE_RANGE_DICT = {
  223. "YYYY": DATE_YYYY_LIMIT,
  224. "MM": DATE_MM_LIMIT,
  225. "DD": DATE_DD_LIMIT,
  226. "Www": DATE_WWW_LIMIT,
  227. "D": DATE_D_LIMIT,
  228. "DDD": DATE_DDD_LIMIT,
  229. }
  230. TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT}
  231. DURATION_RANGE_DICT = {
  232. "PnY": DURATION_PNY_LIMIT,
  233. "PnM": DURATION_PNM_LIMIT,
  234. "PnW": DURATION_PNW_LIMIT,
  235. "PnD": DURATION_PND_LIMIT,
  236. "TnH": DURATION_TNH_LIMIT,
  237. "TnM": DURATION_TNM_LIMIT,
  238. "TnS": DURATION_TNS_LIMIT,
  239. }
  240. REPEATING_INTERVAL_RANGE_DICT = {"Rnn": INTERVAL_RNN_LIMIT}
  241. TIMEZONE_RANGE_DICT = {"hh": TZ_HH_LIMIT, "mm": TZ_MM_LIMIT}
  242. LEAP_SECONDS_SUPPORTED = False
  243. @classmethod
  244. def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None):
  245. raise NotImplementedError
  246. @classmethod
  247. def build_time(cls, hh=None, mm=None, ss=None, tz=None):
  248. raise NotImplementedError
  249. @classmethod
  250. def build_datetime(cls, date, time):
  251. raise NotImplementedError
  252. @classmethod
  253. def build_duration(
  254. cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
  255. ):
  256. raise NotImplementedError
  257. @classmethod
  258. def build_interval(cls, start=None, end=None, duration=None):
  259. # start, end, and duration are all tuples
  260. raise NotImplementedError
  261. @classmethod
  262. def build_repeating_interval(cls, R=None, Rnn=None, interval=None):
  263. # interval is a tuple
  264. raise NotImplementedError
  265. @classmethod
  266. def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""):
  267. raise NotImplementedError
  268. @classmethod
  269. def range_check_date(
  270. cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None, rangedict=None
  271. ):
  272. if rangedict is None:
  273. rangedict = cls.DATE_RANGE_DICT
  274. if "YYYY" in rangedict:
  275. YYYY = rangedict["YYYY"].rangefunc(YYYY, rangedict["YYYY"])
  276. if "MM" in rangedict:
  277. MM = rangedict["MM"].rangefunc(MM, rangedict["MM"])
  278. if "DD" in rangedict:
  279. DD = rangedict["DD"].rangefunc(DD, rangedict["DD"])
  280. if "Www" in rangedict:
  281. Www = rangedict["Www"].rangefunc(Www, rangedict["Www"])
  282. if "D" in rangedict:
  283. D = rangedict["D"].rangefunc(D, rangedict["D"])
  284. if "DDD" in rangedict:
  285. DDD = rangedict["DDD"].rangefunc(DDD, rangedict["DDD"])
  286. if DD is not None:
  287. # Check calendar
  288. if DD > calendar.monthrange(YYYY, MM)[1]:
  289. raise DayOutOfBoundsError(
  290. "{0} is out of range for {1}-{2}".format(DD, YYYY, MM)
  291. )
  292. if DDD is not None:
  293. if calendar.isleap(YYYY) is False and DDD == 366:
  294. raise DayOutOfBoundsError(
  295. "{0} is only valid for leap year.".format(DDD)
  296. )
  297. return (YYYY, MM, DD, Www, D, DDD)
  298. @classmethod
  299. def range_check_time(cls, hh=None, mm=None, ss=None, tz=None, rangedict=None):
  300. # Used for midnight and leap second handling
  301. midnight = False # Handle hh = '24' specially
  302. if rangedict is None:
  303. rangedict = cls.TIME_RANGE_DICT
  304. if "hh" in rangedict:
  305. try:
  306. hh = rangedict["hh"].rangefunc(hh, rangedict["hh"])
  307. except HoursOutOfBoundsError as e:
  308. if float(hh) > 24 and float(hh) < 25:
  309. raise MidnightBoundsError("Hour 24 may only represent midnight.")
  310. raise e
  311. if "mm" in rangedict:
  312. mm = rangedict["mm"].rangefunc(mm, rangedict["mm"])
  313. if "ss" in rangedict:
  314. ss = rangedict["ss"].rangefunc(ss, rangedict["ss"])
  315. if hh is not None and hh == 24:
  316. midnight = True
  317. # Handle midnight range
  318. if midnight is True and (
  319. (mm is not None and mm != 0) or (ss is not None and ss != 0)
  320. ):
  321. raise MidnightBoundsError("Hour 24 may only represent midnight.")
  322. if cls.LEAP_SECONDS_SUPPORTED is True:
  323. if hh != 23 and mm != 59 and ss == 60:
  324. raise cls.TIME_SS_LIMIT.rangeexception(
  325. cls.TIME_SS_LIMIT.rangeerrorstring
  326. )
  327. else:
  328. if hh == 23 and mm == 59 and ss == 60:
  329. # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is
  330. raise LeapSecondError("Leap seconds are not supported.")
  331. if ss == 60:
  332. raise cls.TIME_SS_LIMIT.rangeexception(
  333. cls.TIME_SS_LIMIT.rangeerrorstring
  334. )
  335. return (hh, mm, ss, tz)
  336. @classmethod
  337. def range_check_duration(
  338. cls,
  339. PnY=None,
  340. PnM=None,
  341. PnW=None,
  342. PnD=None,
  343. TnH=None,
  344. TnM=None,
  345. TnS=None,
  346. rangedict=None,
  347. ):
  348. if rangedict is None:
  349. rangedict = cls.DURATION_RANGE_DICT
  350. if "PnY" in rangedict:
  351. PnY = rangedict["PnY"].rangefunc(PnY, rangedict["PnY"])
  352. if "PnM" in rangedict:
  353. PnM = rangedict["PnM"].rangefunc(PnM, rangedict["PnM"])
  354. if "PnW" in rangedict:
  355. PnW = rangedict["PnW"].rangefunc(PnW, rangedict["PnW"])
  356. if "PnD" in rangedict:
  357. PnD = rangedict["PnD"].rangefunc(PnD, rangedict["PnD"])
  358. if "TnH" in rangedict:
  359. TnH = rangedict["TnH"].rangefunc(TnH, rangedict["TnH"])
  360. if "TnM" in rangedict:
  361. TnM = rangedict["TnM"].rangefunc(TnM, rangedict["TnM"])
  362. if "TnS" in rangedict:
  363. TnS = rangedict["TnS"].rangefunc(TnS, rangedict["TnS"])
  364. return (PnY, PnM, PnW, PnD, TnH, TnM, TnS)
  365. @classmethod
  366. def range_check_repeating_interval(
  367. cls, R=None, Rnn=None, interval=None, rangedict=None
  368. ):
  369. if rangedict is None:
  370. rangedict = cls.REPEATING_INTERVAL_RANGE_DICT
  371. if "Rnn" in rangedict:
  372. Rnn = rangedict["Rnn"].rangefunc(Rnn, rangedict["Rnn"])
  373. return (R, Rnn, interval)
  374. @classmethod
  375. def range_check_timezone(
  376. cls, negative=None, Z=None, hh=None, mm=None, name="", rangedict=None
  377. ):
  378. if rangedict is None:
  379. rangedict = cls.TIMEZONE_RANGE_DICT
  380. if "hh" in rangedict:
  381. hh = rangedict["hh"].rangefunc(hh, rangedict["hh"])
  382. if "mm" in rangedict:
  383. mm = rangedict["mm"].rangefunc(mm, rangedict["mm"])
  384. return (negative, Z, hh, mm, name)
  385. @classmethod
  386. def _build_object(cls, parsetuple):
  387. # Given a TupleBuilder tuple, build the correct object
  388. if type(parsetuple) is DateTuple:
  389. return cls.build_date(
  390. YYYY=parsetuple.YYYY,
  391. MM=parsetuple.MM,
  392. DD=parsetuple.DD,
  393. Www=parsetuple.Www,
  394. D=parsetuple.D,
  395. DDD=parsetuple.DDD,
  396. )
  397. if type(parsetuple) is TimeTuple:
  398. return cls.build_time(
  399. hh=parsetuple.hh, mm=parsetuple.mm, ss=parsetuple.ss, tz=parsetuple.tz
  400. )
  401. if type(parsetuple) is DatetimeTuple:
  402. return cls.build_datetime(parsetuple.date, parsetuple.time)
  403. if type(parsetuple) is DurationTuple:
  404. return cls.build_duration(
  405. PnY=parsetuple.PnY,
  406. PnM=parsetuple.PnM,
  407. PnW=parsetuple.PnW,
  408. PnD=parsetuple.PnD,
  409. TnH=parsetuple.TnH,
  410. TnM=parsetuple.TnM,
  411. TnS=parsetuple.TnS,
  412. )
  413. if type(parsetuple) is IntervalTuple:
  414. return cls.build_interval(
  415. start=parsetuple.start, end=parsetuple.end, duration=parsetuple.duration
  416. )
  417. if type(parsetuple) is RepeatingIntervalTuple:
  418. return cls.build_repeating_interval(
  419. R=parsetuple.R, Rnn=parsetuple.Rnn, interval=parsetuple.interval
  420. )
  421. return cls.build_timezone(
  422. negative=parsetuple.negative,
  423. Z=parsetuple.Z,
  424. hh=parsetuple.hh,
  425. mm=parsetuple.mm,
  426. name=parsetuple.name,
  427. )
  428. @classmethod
  429. def _is_interval_end_concise(cls, endtuple):
  430. if type(endtuple) is TimeTuple:
  431. return True
  432. if type(endtuple) is DatetimeTuple:
  433. enddatetuple = endtuple.date
  434. else:
  435. enddatetuple = endtuple
  436. if enddatetuple.YYYY is None:
  437. return True
  438. return False
  439. @classmethod
  440. def _combine_concise_interval_tuples(cls, starttuple, conciseendtuple):
  441. starttimetuple = None
  442. startdatetuple = None
  443. endtimetuple = None
  444. enddatetuple = None
  445. if type(starttuple) is DateTuple:
  446. startdatetuple = starttuple
  447. else:
  448. # Start is a datetime
  449. starttimetuple = starttuple.time
  450. startdatetuple = starttuple.date
  451. if type(conciseendtuple) is DateTuple:
  452. enddatetuple = conciseendtuple
  453. elif type(conciseendtuple) is DatetimeTuple:
  454. enddatetuple = conciseendtuple.date
  455. endtimetuple = conciseendtuple.time
  456. else:
  457. # Time
  458. endtimetuple = conciseendtuple
  459. if enddatetuple is not None:
  460. if enddatetuple.YYYY is None and enddatetuple.MM is None:
  461. newenddatetuple = DateTuple(
  462. YYYY=startdatetuple.YYYY,
  463. MM=startdatetuple.MM,
  464. DD=enddatetuple.DD,
  465. Www=enddatetuple.Www,
  466. D=enddatetuple.D,
  467. DDD=enddatetuple.DDD,
  468. )
  469. else:
  470. newenddatetuple = DateTuple(
  471. YYYY=startdatetuple.YYYY,
  472. MM=enddatetuple.MM,
  473. DD=enddatetuple.DD,
  474. Www=enddatetuple.Www,
  475. D=enddatetuple.D,
  476. DDD=enddatetuple.DDD,
  477. )
  478. if (starttimetuple is not None and starttimetuple.tz is not None) and (
  479. endtimetuple is not None and endtimetuple.tz != starttimetuple.tz
  480. ):
  481. # Copy the timezone across
  482. endtimetuple = TimeTuple(
  483. hh=endtimetuple.hh,
  484. mm=endtimetuple.mm,
  485. ss=endtimetuple.ss,
  486. tz=starttimetuple.tz,
  487. )
  488. if enddatetuple is not None and endtimetuple is None:
  489. return newenddatetuple
  490. if enddatetuple is not None and endtimetuple is not None:
  491. return TupleBuilder.build_datetime(newenddatetuple, endtimetuple)
  492. return TupleBuilder.build_datetime(startdatetuple, endtimetuple)
  493. class TupleBuilder(BaseTimeBuilder):
  494. # Builder used to return the arguments as a tuple, cleans up some parse methods
  495. @classmethod
  496. def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None):
  497. return DateTuple(YYYY, MM, DD, Www, D, DDD)
  498. @classmethod
  499. def build_time(cls, hh=None, mm=None, ss=None, tz=None):
  500. return TimeTuple(hh, mm, ss, tz)
  501. @classmethod
  502. def build_datetime(cls, date, time):
  503. return DatetimeTuple(date, time)
  504. @classmethod
  505. def build_duration(
  506. cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None
  507. ):
  508. return DurationTuple(PnY, PnM, PnW, PnD, TnH, TnM, TnS)
  509. @classmethod
  510. def build_interval(cls, start=None, end=None, duration=None):
  511. return IntervalTuple(start, end, duration)
  512. @classmethod
  513. def build_repeating_interval(cls, R=None, Rnn=None, interval=None):
  514. return RepeatingIntervalTuple(R, Rnn, interval)
  515. @classmethod
  516. def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""):
  517. return TimezoneTuple(negative, Z, hh, mm, name)