isodates.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """
  2. This modules provides a method to parse an ISO 8601:2004 date string to a
  3. python datetime.date instance.
  4. It supports all basic, extended and expanded formats as described in the ISO
  5. standard. The only limitations it has, are given by the Python datetime.date
  6. implementation, which does not support dates before 0001-01-01.
  7. """
  8. import re
  9. from datetime import date, timedelta
  10. from isodate.isoerror import ISO8601Error
  11. from isodate.isostrf import DATE_EXT_COMPLETE, strftime
  12. DATE_REGEX_CACHE = {}
  13. # A dictionary to cache pre-compiled regular expressions.
  14. # A set of regular expressions is identified, by number of year digits allowed
  15. # and whether a plus/minus sign is required or not. (This option is changeable
  16. # only for 4 digit years).
  17. def build_date_regexps(yeardigits=4, expanded=False):
  18. """
  19. Compile set of regular expressions to parse ISO dates. The expressions will
  20. be created only if they are not already in REGEX_CACHE.
  21. It is necessary to fix the number of year digits, else it is not possible
  22. to automatically distinguish between various ISO date formats.
  23. ISO 8601 allows more than 4 digit years, on prior agreement, but then a +/-
  24. sign is required (expanded format). To support +/- sign for 4 digit years,
  25. the expanded parameter needs to be set to True.
  26. """
  27. if yeardigits != 4:
  28. expanded = True
  29. if (yeardigits, expanded) not in DATE_REGEX_CACHE:
  30. cache_entry = []
  31. # ISO 8601 expanded DATE formats allow an arbitrary number of year
  32. # digits with a leading +/- sign.
  33. if expanded:
  34. sign = 1
  35. else:
  36. sign = 0
  37. def add_re(regex_text):
  38. cache_entry.append(re.compile(r"\A" + regex_text + r"\Z"))
  39. # 1. complete dates:
  40. # YYYY-MM-DD or +- YYYYYY-MM-DD... extended date format
  41. add_re(
  42. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
  43. r"-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})" % (sign, yeardigits)
  44. )
  45. # YYYYMMDD or +- YYYYYYMMDD... basic date format
  46. add_re(
  47. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
  48. r"(?P<month>[0-9]{2})(?P<day>[0-9]{2})" % (sign, yeardigits)
  49. )
  50. # 2. complete week dates:
  51. # YYYY-Www-D or +-YYYYYY-Www-D ... extended week date
  52. add_re(
  53. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
  54. r"-W(?P<week>[0-9]{2})-(?P<day>[0-9]{1})" % (sign, yeardigits)
  55. )
  56. # YYYYWwwD or +-YYYYYYWwwD ... basic week date
  57. add_re(
  58. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
  59. r"(?P<week>[0-9]{2})(?P<day>[0-9]{1})" % (sign, yeardigits)
  60. )
  61. # 3. ordinal dates:
  62. # YYYY-DDD or +-YYYYYY-DDD ... extended format
  63. add_re(
  64. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
  65. r"-(?P<day>[0-9]{3})" % (sign, yeardigits)
  66. )
  67. # YYYYDDD or +-YYYYYYDDD ... basic format
  68. add_re(
  69. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
  70. r"(?P<day>[0-9]{3})" % (sign, yeardigits)
  71. )
  72. # 4. week dates:
  73. # YYYY-Www or +-YYYYYY-Www ... extended reduced accuracy week date
  74. # 4. week dates:
  75. # YYYY-Www or +-YYYYYY-Www ... extended reduced accuracy week date
  76. add_re(
  77. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
  78. r"-W(?P<week>[0-9]{2})" % (sign, yeardigits)
  79. )
  80. # YYYYWww or +-YYYYYYWww ... basic reduced accuracy week date
  81. add_re(
  82. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
  83. r"(?P<week>[0-9]{2})" % (sign, yeardigits)
  84. )
  85. # 5. month dates:
  86. # YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month
  87. # 5. month dates:
  88. # YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month
  89. add_re(
  90. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
  91. r"-(?P<month>[0-9]{2})" % (sign, yeardigits)
  92. )
  93. # YYYMM or +-YYYYYYMM ... basic incomplete month date format
  94. add_re(
  95. r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
  96. r"(?P<month>[0-9]{2})" % (sign, yeardigits)
  97. )
  98. # 6. year dates:
  99. # YYYY or +-YYYYYY ... reduced accuracy specific year
  100. add_re(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})" % (sign, yeardigits))
  101. # 7. century dates:
  102. # YY or +-YYYY ... reduced accuracy specific century
  103. add_re(r"(?P<sign>[+-]){%d}" r"(?P<century>[0-9]{%d})" % (sign, yeardigits - 2))
  104. DATE_REGEX_CACHE[(yeardigits, expanded)] = cache_entry
  105. return DATE_REGEX_CACHE[(yeardigits, expanded)]
  106. def parse_date(datestring, yeardigits=4, expanded=False, defaultmonth=1, defaultday=1):
  107. """
  108. Parse an ISO 8601 date string into a datetime.date object.
  109. As the datetime.date implementation is limited to dates starting from
  110. 0001-01-01, negative dates (BC) and year 0 can not be parsed by this
  111. method.
  112. For incomplete dates, this method chooses the first day for it. For
  113. instance if only a century is given, this method returns the 1st of
  114. January in year 1 of this century.
  115. supported formats: (expanded formats are shown with 6 digits for year)
  116. YYYYMMDD +-YYYYYYMMDD basic complete date
  117. YYYY-MM-DD +-YYYYYY-MM-DD extended complete date
  118. YYYYWwwD +-YYYYYYWwwD basic complete week date
  119. YYYY-Www-D +-YYYYYY-Www-D extended complete week date
  120. YYYYDDD +-YYYYYYDDD basic ordinal date
  121. YYYY-DDD +-YYYYYY-DDD extended ordinal date
  122. YYYYWww +-YYYYYYWww basic incomplete week date
  123. YYYY-Www +-YYYYYY-Www extended incomplete week date
  124. YYYMM +-YYYYYYMM basic incomplete month date
  125. YYY-MM +-YYYYYY-MM incomplete month date
  126. YYYY +-YYYYYY incomplete year date
  127. YY +-YYYY incomplete century date
  128. @param datestring: the ISO date string to parse
  129. @param yeardigits: how many digits are used to represent a year
  130. @param expanded: if True then +/- signs are allowed. This parameter
  131. is forced to True, if yeardigits != 4
  132. @return: a datetime.date instance represented by datestring
  133. @raise ISO8601Error: if this function can not parse the datestring
  134. @raise ValueError: if datestring can not be represented by datetime.date
  135. """
  136. if yeardigits != 4:
  137. expanded = True
  138. isodates = build_date_regexps(yeardigits, expanded)
  139. for pattern in isodates:
  140. match = pattern.match(datestring)
  141. if match:
  142. groups = match.groupdict()
  143. # sign, century, year, month, week, day,
  144. # FIXME: negative dates not possible with python standard types
  145. sign = (groups["sign"] == "-" and -1) or 1
  146. if "century" in groups:
  147. return date(
  148. sign * (int(groups["century"]) * 100 + 1), defaultmonth, defaultday
  149. )
  150. if "month" not in groups: # weekdate or ordinal date
  151. ret = date(sign * int(groups["year"]), 1, 1)
  152. if "week" in groups:
  153. isotuple = ret.isocalendar()
  154. if "day" in groups:
  155. days = int(groups["day"] or 1)
  156. else:
  157. days = 1
  158. # if first week in year, do weeks-1
  159. return ret + timedelta(
  160. weeks=int(groups["week"]) - (((isotuple[1] == 1) and 1) or 0),
  161. days=-isotuple[2] + days,
  162. )
  163. elif "day" in groups: # ordinal date
  164. return ret + timedelta(days=int(groups["day"]) - 1)
  165. else: # year date
  166. return ret.replace(month=defaultmonth, day=defaultday)
  167. # year-, month-, or complete date
  168. if "day" not in groups or groups["day"] is None:
  169. day = defaultday
  170. else:
  171. day = int(groups["day"])
  172. return date(
  173. sign * int(groups["year"]), int(groups["month"]) or defaultmonth, day
  174. )
  175. raise ISO8601Error("Unrecognised ISO 8601 date format: %r" % datestring)
  176. def date_isoformat(tdate, format=DATE_EXT_COMPLETE, yeardigits=4):
  177. """
  178. Format date strings.
  179. This method is just a wrapper around isodate.isostrf.strftime and uses
  180. Date-Extended-Complete as default format.
  181. """
  182. return strftime(tdate, format, yeardigits)