comparison env/lib/python3.7/site-packages/isodate/isodates.py @ 5:9b1c78e6ba9c draft default tip

"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
author shellac
date Mon, 01 Jun 2020 08:59:25 -0400
parents 79f47841a781
children
comparison
equal deleted inserted replaced
4:79f47841a781 5:9b1c78e6ba9c
1 ##############################################################################
2 # Copyright 2009, Gerhard Weis
3 # All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are met:
7 #
8 # * Redistributions of source code must retain the above copyright notice,
9 # this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above copyright notice,
11 # this list of conditions and the following disclaimer in the documentation
12 # and/or other materials provided with the distribution.
13 # * Neither the name of the authors nor the names of its contributors
14 # may be used to endorse or promote products derived from this software
15 # without specific prior written permission.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 # CONTRACT, STRICT LIABILITY, OR TORT
26 ##############################################################################
27 '''
28 This modules provides a method to parse an ISO 8601:2004 date string to a
29 python datetime.date instance.
30
31 It supports all basic, extended and expanded formats as described in the ISO
32 standard. The only limitations it has, are given by the Python datetime.date
33 implementation, which does not support dates before 0001-01-01.
34 '''
35 import re
36 from datetime import date, timedelta
37
38 from isodate.isostrf import strftime, DATE_EXT_COMPLETE
39 from isodate.isoerror import ISO8601Error
40
41 DATE_REGEX_CACHE = {}
42 # A dictionary to cache pre-compiled regular expressions.
43 # A set of regular expressions is identified, by number of year digits allowed
44 # and whether a plus/minus sign is required or not. (This option is changeable
45 # only for 4 digit years).
46
47
48 def build_date_regexps(yeardigits=4, expanded=False):
49 '''
50 Compile set of regular expressions to parse ISO dates. The expressions will
51 be created only if they are not already in REGEX_CACHE.
52
53 It is necessary to fix the number of year digits, else it is not possible
54 to automatically distinguish between various ISO date formats.
55
56 ISO 8601 allows more than 4 digit years, on prior agreement, but then a +/-
57 sign is required (expanded format). To support +/- sign for 4 digit years,
58 the expanded parameter needs to be set to True.
59 '''
60 if yeardigits != 4:
61 expanded = True
62 if (yeardigits, expanded) not in DATE_REGEX_CACHE:
63 cache_entry = []
64 # ISO 8601 expanded DATE formats allow an arbitrary number of year
65 # digits with a leading +/- sign.
66 if expanded:
67 sign = 1
68 else:
69 sign = 0
70 # 1. complete dates:
71 # YYYY-MM-DD or +- YYYYYY-MM-DD... extended date format
72 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
73 r"-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})"
74 % (sign, yeardigits)))
75 # YYYYMMDD or +- YYYYYYMMDD... basic date format
76 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
77 r"(?P<month>[0-9]{2})(?P<day>[0-9]{2})"
78 % (sign, yeardigits)))
79 # 2. complete week dates:
80 # YYYY-Www-D or +-YYYYYY-Www-D ... extended week date
81 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
82 r"-W(?P<week>[0-9]{2})-(?P<day>[0-9]{1})"
83 % (sign, yeardigits)))
84 # YYYYWwwD or +-YYYYYYWwwD ... basic week date
85 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
86 r"(?P<week>[0-9]{2})(?P<day>[0-9]{1})"
87 % (sign, yeardigits)))
88 # 3. ordinal dates:
89 # YYYY-DDD or +-YYYYYY-DDD ... extended format
90 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
91 r"-(?P<day>[0-9]{3})"
92 % (sign, yeardigits)))
93 # YYYYDDD or +-YYYYYYDDD ... basic format
94 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
95 r"(?P<day>[0-9]{3})"
96 % (sign, yeardigits)))
97 # 4. week dates:
98 # YYYY-Www or +-YYYYYY-Www ... extended reduced accuracy week date
99 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
100 r"-W(?P<week>[0-9]{2})"
101 % (sign, yeardigits)))
102 # YYYYWww or +-YYYYYYWww ... basic reduced accuracy week date
103 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
104 r"(?P<week>[0-9]{2})"
105 % (sign, yeardigits)))
106 # 5. month dates:
107 # YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month
108 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
109 r"-(?P<month>[0-9]{2})"
110 % (sign, yeardigits)))
111 # YYYMM or +-YYYYYYMM ... basic incomplete month date format
112 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
113 r"(?P<month>[0-9]{2})"
114 % (sign, yeardigits)))
115 # 6. year dates:
116 # YYYY or +-YYYYYY ... reduced accuracy specific year
117 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
118 % (sign, yeardigits)))
119 # 7. century dates:
120 # YY or +-YYYY ... reduced accuracy specific century
121 cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}"
122 r"(?P<century>[0-9]{%d})"
123 % (sign, yeardigits - 2)))
124
125 DATE_REGEX_CACHE[(yeardigits, expanded)] = cache_entry
126 return DATE_REGEX_CACHE[(yeardigits, expanded)]
127
128
129 def parse_date(
130 datestring,
131 yeardigits=4, expanded=False, defaultmonth=1, defaultday=1):
132 '''
133 Parse an ISO 8601 date string into a datetime.date object.
134
135 As the datetime.date implementation is limited to dates starting from
136 0001-01-01, negative dates (BC) and year 0 can not be parsed by this
137 method.
138
139 For incomplete dates, this method chooses the first day for it. For
140 instance if only a century is given, this method returns the 1st of
141 January in year 1 of this century.
142
143 supported formats: (expanded formats are shown with 6 digits for year)
144 YYYYMMDD +-YYYYYYMMDD basic complete date
145 YYYY-MM-DD +-YYYYYY-MM-DD extended complete date
146 YYYYWwwD +-YYYYYYWwwD basic complete week date
147 YYYY-Www-D +-YYYYYY-Www-D extended complete week date
148 YYYYDDD +-YYYYYYDDD basic ordinal date
149 YYYY-DDD +-YYYYYY-DDD extended ordinal date
150 YYYYWww +-YYYYYYWww basic incomplete week date
151 YYYY-Www +-YYYYYY-Www extended incomplete week date
152 YYYMM +-YYYYYYMM basic incomplete month date
153 YYY-MM +-YYYYYY-MM incomplete month date
154 YYYY +-YYYYYY incomplete year date
155 YY +-YYYY incomplete century date
156
157 @param datestring: the ISO date string to parse
158 @param yeardigits: how many digits are used to represent a year
159 @param expanded: if True then +/- signs are allowed. This parameter
160 is forced to True, if yeardigits != 4
161
162 @return: a datetime.date instance represented by datestring
163 @raise ISO8601Error: if this function can not parse the datestring
164 @raise ValueError: if datestring can not be represented by datetime.date
165 '''
166 if yeardigits != 4:
167 expanded = True
168 isodates = build_date_regexps(yeardigits, expanded)
169 for pattern in isodates:
170 match = pattern.match(datestring)
171 if match:
172 groups = match.groupdict()
173 # sign, century, year, month, week, day,
174 # FIXME: negative dates not possible with python standard types
175 sign = (groups['sign'] == '-' and -1) or 1
176 if 'century' in groups:
177 return date(
178 sign * (int(groups['century']) * 100 + 1),
179 defaultmonth, defaultday)
180 if 'month' not in groups: # weekdate or ordinal date
181 ret = date(sign * int(groups['year']), 1, 1)
182 if 'week' in groups:
183 isotuple = ret.isocalendar()
184 if 'day' in groups:
185 days = int(groups['day'] or 1)
186 else:
187 days = 1
188 # if first week in year, do weeks-1
189 return ret + timedelta(weeks=int(groups['week']) -
190 (((isotuple[1] == 1) and 1) or 0),
191 days=-isotuple[2] + days)
192 elif 'day' in groups: # ordinal date
193 return ret + timedelta(days=int(groups['day']) - 1)
194 else: # year date
195 return ret.replace(month=defaultmonth, day=defaultday)
196 # year-, month-, or complete date
197 if 'day' not in groups or groups['day'] is None:
198 day = defaultday
199 else:
200 day = int(groups['day'])
201 return date(sign * int(groups['year']),
202 int(groups['month']) or defaultmonth, day)
203 raise ISO8601Error('Unrecognised ISO 8601 date format: %r' % datestring)
204
205
206 def date_isoformat(tdate, format=DATE_EXT_COMPLETE, yeardigits=4):
207 '''
208 Format date strings.
209
210 This method is just a wrapper around isodate.isostrf.strftime and uses
211 Date-Extended-Complete as default format.
212 '''
213 return strftime(tdate, format, yeardigits)