interval.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  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. from aniso8601.builders import DatetimeTuple, DateTuple, TupleBuilder
  8. from aniso8601.builders.python import PythonTimeBuilder
  9. from aniso8601.compat import is_string
  10. from aniso8601.date import parse_date
  11. from aniso8601.duration import parse_duration
  12. from aniso8601.exceptions import ISOFormatError
  13. from aniso8601.resolution import IntervalResolution
  14. from aniso8601.time import parse_datetime, parse_time
  15. def get_interval_resolution(
  16. isointervalstr, intervaldelimiter="/", datetimedelimiter="T"
  17. ):
  18. isointervaltuple = parse_interval(
  19. isointervalstr,
  20. intervaldelimiter=intervaldelimiter,
  21. datetimedelimiter=datetimedelimiter,
  22. builder=TupleBuilder,
  23. )
  24. return _get_interval_resolution(isointervaltuple)
  25. def get_repeating_interval_resolution(
  26. isointervalstr, intervaldelimiter="/", datetimedelimiter="T"
  27. ):
  28. repeatingintervaltuple = parse_repeating_interval(
  29. isointervalstr,
  30. intervaldelimiter=intervaldelimiter,
  31. datetimedelimiter=datetimedelimiter,
  32. builder=TupleBuilder,
  33. )
  34. return _get_interval_resolution(repeatingintervaltuple.interval)
  35. def _get_interval_resolution(intervaltuple):
  36. if intervaltuple.start is not None and intervaltuple.end is not None:
  37. return max(
  38. _get_interval_component_resolution(intervaltuple.start),
  39. _get_interval_component_resolution(intervaltuple.end),
  40. )
  41. if intervaltuple.start is not None and intervaltuple.duration is not None:
  42. return max(
  43. _get_interval_component_resolution(intervaltuple.start),
  44. _get_interval_component_resolution(intervaltuple.duration),
  45. )
  46. return max(
  47. _get_interval_component_resolution(intervaltuple.end),
  48. _get_interval_component_resolution(intervaltuple.duration),
  49. )
  50. def _get_interval_component_resolution(componenttuple):
  51. if type(componenttuple) is DateTuple:
  52. if componenttuple.DDD is not None:
  53. # YYYY-DDD
  54. # YYYYDDD
  55. return IntervalResolution.Ordinal
  56. if componenttuple.D is not None:
  57. # YYYY-Www-D
  58. # YYYYWwwD
  59. return IntervalResolution.Weekday
  60. if componenttuple.Www is not None:
  61. # YYYY-Www
  62. # YYYYWww
  63. return IntervalResolution.Week
  64. if componenttuple.DD is not None:
  65. # YYYY-MM-DD
  66. # YYYYMMDD
  67. return IntervalResolution.Day
  68. if componenttuple.MM is not None:
  69. # YYYY-MM
  70. return IntervalResolution.Month
  71. # Y[YYY]
  72. return IntervalResolution.Year
  73. elif type(componenttuple) is DatetimeTuple:
  74. # Datetime
  75. if componenttuple.time.ss is not None:
  76. return IntervalResolution.Seconds
  77. if componenttuple.time.mm is not None:
  78. return IntervalResolution.Minutes
  79. return IntervalResolution.Hours
  80. # Duration
  81. if componenttuple.TnS is not None:
  82. return IntervalResolution.Seconds
  83. if componenttuple.TnM is not None:
  84. return IntervalResolution.Minutes
  85. if componenttuple.TnH is not None:
  86. return IntervalResolution.Hours
  87. if componenttuple.PnD is not None:
  88. return IntervalResolution.Day
  89. if componenttuple.PnW is not None:
  90. return IntervalResolution.Week
  91. if componenttuple.PnM is not None:
  92. return IntervalResolution.Month
  93. return IntervalResolution.Year
  94. def parse_interval(
  95. isointervalstr,
  96. intervaldelimiter="/",
  97. datetimedelimiter="T",
  98. builder=PythonTimeBuilder,
  99. ):
  100. # Given a string representing an ISO 8601 interval, return an
  101. # interval built by the given builder. Valid formats are:
  102. #
  103. # <start>/<end>
  104. # <start>/<duration>
  105. # <duration>/<end>
  106. #
  107. # The <start> and <end> values can represent dates, or datetimes,
  108. # not times.
  109. #
  110. # The format:
  111. #
  112. # <duration>
  113. #
  114. # Is expressly not supported as there is no way to provide the additional
  115. # required context.
  116. if is_string(isointervalstr) is False:
  117. raise ValueError("Interval must be string.")
  118. if len(isointervalstr) == 0:
  119. raise ISOFormatError("Interval string is empty.")
  120. if isointervalstr[0] == "R":
  121. raise ISOFormatError(
  122. "ISO 8601 repeating intervals must be parsed "
  123. "with parse_repeating_interval."
  124. )
  125. intervaldelimitercount = isointervalstr.count(intervaldelimiter)
  126. if intervaldelimitercount == 0:
  127. raise ISOFormatError(
  128. 'Interval delimiter "{0}" is not in interval '
  129. 'string "{1}".'.format(intervaldelimiter, isointervalstr)
  130. )
  131. if intervaldelimitercount > 1:
  132. raise ISOFormatError(
  133. "{0} is not a valid ISO 8601 interval".format(isointervalstr)
  134. )
  135. return _parse_interval(
  136. isointervalstr, builder, intervaldelimiter, datetimedelimiter
  137. )
  138. def parse_repeating_interval(
  139. isointervalstr,
  140. intervaldelimiter="/",
  141. datetimedelimiter="T",
  142. builder=PythonTimeBuilder,
  143. ):
  144. # Given a string representing an ISO 8601 interval repeating, return an
  145. # interval built by the given builder. Valid formats are:
  146. #
  147. # Rnn/<interval>
  148. # R/<interval>
  149. if not isinstance(isointervalstr, str):
  150. raise ValueError("Interval must be string.")
  151. if len(isointervalstr) == 0:
  152. raise ISOFormatError("Repeating interval string is empty.")
  153. if isointervalstr[0] != "R":
  154. raise ISOFormatError("ISO 8601 repeating interval must start " "with an R.")
  155. if intervaldelimiter not in isointervalstr:
  156. raise ISOFormatError(
  157. 'Interval delimiter "{0}" is not in interval '
  158. 'string "{1}".'.format(intervaldelimiter, isointervalstr)
  159. )
  160. # Parse the number of iterations
  161. iterationpart, intervalpart = isointervalstr.split(intervaldelimiter, 1)
  162. if len(iterationpart) > 1:
  163. R = False
  164. Rnn = iterationpart[1:]
  165. else:
  166. R = True
  167. Rnn = None
  168. interval = _parse_interval(
  169. intervalpart, TupleBuilder, intervaldelimiter, datetimedelimiter
  170. )
  171. return builder.build_repeating_interval(R=R, Rnn=Rnn, interval=interval)
  172. def _parse_interval(
  173. isointervalstr, builder, intervaldelimiter="/", datetimedelimiter="T"
  174. ):
  175. # Returns a tuple containing the start of the interval, the end of the
  176. # interval, and or the interval duration
  177. firstpart, secondpart = isointervalstr.split(intervaldelimiter)
  178. if len(firstpart) == 0 or len(secondpart) == 0:
  179. raise ISOFormatError(
  180. "{0} is not a valid ISO 8601 interval".format(isointervalstr)
  181. )
  182. if firstpart[0] == "P":
  183. # <duration>/<end>
  184. # Notice that these are not returned 'in order' (earlier to later), this
  185. # is to maintain consistency with parsing <start>/<end> durations, as
  186. # well as making repeating interval code cleaner. Users who desire
  187. # durations to be in order can use the 'sorted' operator.
  188. duration = parse_duration(firstpart, builder=TupleBuilder)
  189. # We need to figure out if <end> is a date, or a datetime
  190. if secondpart.find(datetimedelimiter) != -1:
  191. # <end> is a datetime
  192. endtuple = parse_datetime(
  193. secondpart, delimiter=datetimedelimiter, builder=TupleBuilder
  194. )
  195. else:
  196. endtuple = parse_date(secondpart, builder=TupleBuilder)
  197. return builder.build_interval(end=endtuple, duration=duration)
  198. elif secondpart[0] == "P":
  199. # <start>/<duration>
  200. # We need to figure out if <start> is a date, or a datetime
  201. duration = parse_duration(secondpart, builder=TupleBuilder)
  202. if firstpart.find(datetimedelimiter) != -1:
  203. # <start> is a datetime
  204. starttuple = parse_datetime(
  205. firstpart, delimiter=datetimedelimiter, builder=TupleBuilder
  206. )
  207. else:
  208. # <start> must just be a date
  209. starttuple = parse_date(firstpart, builder=TupleBuilder)
  210. return builder.build_interval(start=starttuple, duration=duration)
  211. # <start>/<end>
  212. if firstpart.find(datetimedelimiter) != -1:
  213. # Both parts are datetimes
  214. starttuple = parse_datetime(
  215. firstpart, delimiter=datetimedelimiter, builder=TupleBuilder
  216. )
  217. else:
  218. starttuple = parse_date(firstpart, builder=TupleBuilder)
  219. endtuple = _parse_interval_end(secondpart, starttuple, datetimedelimiter)
  220. return builder.build_interval(start=starttuple, end=endtuple)
  221. def _parse_interval_end(endstr, starttuple, datetimedelimiter):
  222. datestr = None
  223. timestr = None
  224. monthstr = None
  225. daystr = None
  226. concise = False
  227. if type(starttuple) is DateTuple:
  228. startdatetuple = starttuple
  229. else:
  230. # Start is a datetime
  231. startdatetuple = starttuple.date
  232. if datetimedelimiter in endstr:
  233. datestr, timestr = endstr.split(datetimedelimiter, 1)
  234. elif ":" in endstr:
  235. timestr = endstr
  236. else:
  237. datestr = endstr
  238. if timestr is not None:
  239. endtimetuple = parse_time(timestr, builder=TupleBuilder)
  240. # End is just a time
  241. if datestr is None:
  242. return endtimetuple
  243. # Handle backwards concise representation
  244. if datestr.count("-") == 1:
  245. monthstr, daystr = datestr.split("-")
  246. concise = True
  247. elif len(datestr) <= 2:
  248. daystr = datestr
  249. concise = True
  250. elif len(datestr) <= 4:
  251. monthstr = datestr[0:2]
  252. daystr = datestr[2:]
  253. concise = True
  254. if concise is True:
  255. concisedatestr = startdatetuple.YYYY
  256. # Separators required because concise elements may be missing digits
  257. if monthstr is not None:
  258. concisedatestr += "-" + monthstr
  259. elif startdatetuple.MM is not None:
  260. concisedatestr += "-" + startdatetuple.MM
  261. concisedatestr += "-" + daystr
  262. enddatetuple = parse_date(concisedatestr, builder=TupleBuilder)
  263. # Clear unsupplied components
  264. if monthstr is None:
  265. enddatetuple = TupleBuilder.build_date(DD=enddatetuple.DD)
  266. else:
  267. # Year not provided
  268. enddatetuple = TupleBuilder.build_date(
  269. MM=enddatetuple.MM, DD=enddatetuple.DD
  270. )
  271. else:
  272. enddatetuple = parse_date(datestr, builder=TupleBuilder)
  273. if timestr is None:
  274. return enddatetuple
  275. return TupleBuilder.build_datetime(enddatetuple, endtimetuple)