##// END OF EJS Templates
dateutil: correct default for Ymd in parsedate...
Jun Wu -
r44170:aef7b91d default draft
parent child Browse files
Show More
@@ -1,351 +1,354 b''
1 1 # util.py - Mercurial utility functions relative to dates
2 2 #
3 3 # Copyright 2018 Boris Feld <boris.feld@octobus.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import, print_function
9 9
10 10 import calendar
11 11 import datetime
12 12 import time
13 13
14 14 from ..i18n import _
15 15 from .. import (
16 16 encoding,
17 17 error,
18 18 pycompat,
19 19 )
20 20
21 21 # used by parsedate
22 22 defaultdateformats = (
23 23 b'%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601
24 24 b'%Y-%m-%dT%H:%M', # without seconds
25 25 b'%Y-%m-%dT%H%M%S', # another awful but legal variant without :
26 26 b'%Y-%m-%dT%H%M', # without seconds
27 27 b'%Y-%m-%d %H:%M:%S', # our common legal variant
28 28 b'%Y-%m-%d %H:%M', # without seconds
29 29 b'%Y-%m-%d %H%M%S', # without :
30 30 b'%Y-%m-%d %H%M', # without seconds
31 31 b'%Y-%m-%d %I:%M:%S%p',
32 32 b'%Y-%m-%d %H:%M',
33 33 b'%Y-%m-%d %I:%M%p',
34 34 b'%Y-%m-%d',
35 35 b'%m-%d',
36 36 b'%m/%d',
37 37 b'%m/%d/%y',
38 38 b'%m/%d/%Y',
39 39 b'%a %b %d %H:%M:%S %Y',
40 40 b'%a %b %d %I:%M:%S%p %Y',
41 41 b'%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
42 42 b'%b %d %H:%M:%S %Y',
43 43 b'%b %d %I:%M:%S%p %Y',
44 44 b'%b %d %H:%M:%S',
45 45 b'%b %d %I:%M:%S%p',
46 46 b'%b %d %H:%M',
47 47 b'%b %d %I:%M%p',
48 48 b'%b %d %Y',
49 49 b'%b %d',
50 50 b'%H:%M:%S',
51 51 b'%I:%M:%S%p',
52 52 b'%H:%M',
53 53 b'%I:%M%p',
54 54 )
55 55
56 56 extendeddateformats = defaultdateformats + (b"%Y", b"%Y-%m", b"%b", b"%b %Y",)
57 57
58 58
59 59 def makedate(timestamp=None):
60 60 '''Return a unix timestamp (or the current time) as a (unixtime,
61 61 offset) tuple based off the local timezone.'''
62 62 if timestamp is None:
63 63 timestamp = time.time()
64 64 if timestamp < 0:
65 65 hint = _(b"check your clock")
66 66 raise error.Abort(_(b"negative timestamp: %d") % timestamp, hint=hint)
67 67 delta = datetime.datetime.utcfromtimestamp(
68 68 timestamp
69 69 ) - datetime.datetime.fromtimestamp(timestamp)
70 70 tz = delta.days * 86400 + delta.seconds
71 71 return timestamp, tz
72 72
73 73
74 74 def datestr(date=None, format=b'%a %b %d %H:%M:%S %Y %1%2'):
75 75 """represent a (unixtime, offset) tuple as a localized time.
76 76 unixtime is seconds since the epoch, and offset is the time zone's
77 77 number of seconds away from UTC.
78 78
79 79 >>> datestr((0, 0))
80 80 'Thu Jan 01 00:00:00 1970 +0000'
81 81 >>> datestr((42, 0))
82 82 'Thu Jan 01 00:00:42 1970 +0000'
83 83 >>> datestr((-42, 0))
84 84 'Wed Dec 31 23:59:18 1969 +0000'
85 85 >>> datestr((0x7fffffff, 0))
86 86 'Tue Jan 19 03:14:07 2038 +0000'
87 87 >>> datestr((-0x80000000, 0))
88 88 'Fri Dec 13 20:45:52 1901 +0000'
89 89 """
90 90 t, tz = date or makedate()
91 91 if b"%1" in format or b"%2" in format or b"%z" in format:
92 92 sign = (tz > 0) and b"-" or b"+"
93 93 minutes = abs(tz) // 60
94 94 q, r = divmod(minutes, 60)
95 95 format = format.replace(b"%z", b"%1%2")
96 96 format = format.replace(b"%1", b"%c%02d" % (sign, q))
97 97 format = format.replace(b"%2", b"%02d" % r)
98 98 d = t - tz
99 99 if d > 0x7FFFFFFF:
100 100 d = 0x7FFFFFFF
101 101 elif d < -0x80000000:
102 102 d = -0x80000000
103 103 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
104 104 # because they use the gmtime() system call which is buggy on Windows
105 105 # for negative values.
106 106 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
107 107 s = encoding.strtolocal(t.strftime(encoding.strfromlocal(format)))
108 108 return s
109 109
110 110
111 111 def shortdate(date=None):
112 112 """turn (timestamp, tzoff) tuple into iso 8631 date."""
113 113 return datestr(date, format=b'%Y-%m-%d')
114 114
115 115
116 116 def parsetimezone(s):
117 117 """find a trailing timezone, if any, in string, and return a
118 118 (offset, remainder) pair"""
119 119 s = pycompat.bytestr(s)
120 120
121 121 if s.endswith(b"GMT") or s.endswith(b"UTC"):
122 122 return 0, s[:-3].rstrip()
123 123
124 124 # Unix-style timezones [+-]hhmm
125 125 if len(s) >= 5 and s[-5] in b"+-" and s[-4:].isdigit():
126 126 sign = (s[-5] == b"+") and 1 or -1
127 127 hours = int(s[-4:-2])
128 128 minutes = int(s[-2:])
129 129 return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
130 130
131 131 # ISO8601 trailing Z
132 132 if s.endswith(b"Z") and s[-2:-1].isdigit():
133 133 return 0, s[:-1]
134 134
135 135 # ISO8601-style [+-]hh:mm
136 136 if (
137 137 len(s) >= 6
138 138 and s[-6] in b"+-"
139 139 and s[-3] == b":"
140 140 and s[-5:-3].isdigit()
141 141 and s[-2:].isdigit()
142 142 ):
143 143 sign = (s[-6] == b"+") and 1 or -1
144 144 hours = int(s[-5:-3])
145 145 minutes = int(s[-2:])
146 146 return -sign * (hours * 60 + minutes) * 60, s[:-6]
147 147
148 148 return None, s
149 149
150 150
151 151 def strdate(string, format, defaults=None):
152 152 """parse a localized time string and return a (unixtime, offset) tuple.
153 153 if the string cannot be parsed, ValueError is raised."""
154 154 if defaults is None:
155 155 defaults = {}
156 156
157 157 # NOTE: unixtime = localunixtime + offset
158 158 offset, date = parsetimezone(string)
159 159
160 160 # add missing elements from defaults
161 161 usenow = False # default to using biased defaults
162 162 for part in (
163 163 b"S",
164 164 b"M",
165 165 b"HI",
166 166 b"d",
167 167 b"mb",
168 168 b"yY",
169 169 ): # decreasing specificity
170 170 part = pycompat.bytestr(part)
171 171 found = [True for p in part if (b"%" + p) in format]
172 172 if not found:
173 173 date += b"@" + defaults[part][usenow]
174 174 format += b"@%" + part[0]
175 175 else:
176 176 # We've found a specific time element, less specific time
177 177 # elements are relative to today
178 178 usenow = True
179 179
180 180 timetuple = time.strptime(
181 181 encoding.strfromlocal(date), encoding.strfromlocal(format)
182 182 )
183 183 localunixtime = int(calendar.timegm(timetuple))
184 184 if offset is None:
185 185 # local timezone
186 186 unixtime = int(time.mktime(timetuple))
187 187 offset = unixtime - localunixtime
188 188 else:
189 189 unixtime = localunixtime + offset
190 190 return unixtime, offset
191 191
192 192
193 193 def parsedate(date, formats=None, bias=None):
194 194 """parse a localized date/time and return a (unixtime, offset) tuple.
195 195
196 196 The date may be a "unixtime offset" string or in one of the specified
197 197 formats. If the date already is a (unixtime, offset) tuple, it is returned.
198 198
199 199 >>> parsedate(b' today ') == parsedate(
200 200 ... datetime.date.today().strftime('%b %d').encode('ascii'))
201 201 True
202 202 >>> parsedate(b'yesterday ') == parsedate(
203 203 ... (datetime.date.today() - datetime.timedelta(days=1)
204 204 ... ).strftime('%b %d').encode('ascii'))
205 205 True
206 206 >>> now, tz = makedate()
207 207 >>> strnow, strtz = parsedate(b'now')
208 208 >>> (strnow - now) < 1
209 209 True
210 210 >>> tz == strtz
211 211 True
212 >>> parsedate(b'2000 UTC', formats=extendeddateformats)
213 (946684800, 0)
212 214 """
213 215 if bias is None:
214 216 bias = {}
215 217 if not date:
216 218 return 0, 0
217 219 if isinstance(date, tuple) and len(date) == 2:
218 220 return date
219 221 if not formats:
220 222 formats = defaultdateformats
221 223 date = date.strip()
222 224
223 225 if date == b'now' or date == _(b'now'):
224 226 return makedate()
225 227 if date == b'today' or date == _(b'today'):
226 228 date = datetime.date.today().strftime('%b %d')
227 229 date = encoding.strtolocal(date)
228 230 elif date == b'yesterday' or date == _(b'yesterday'):
229 231 date = (datetime.date.today() - datetime.timedelta(days=1)).strftime(
230 232 r'%b %d'
231 233 )
232 234 date = encoding.strtolocal(date)
233 235
234 236 try:
235 237 when, offset = map(int, date.split(b' '))
236 238 except ValueError:
237 239 # fill out defaults
238 240 now = makedate()
239 241 defaults = {}
240 242 for part in (b"d", b"mb", b"yY", b"HI", b"M", b"S"):
241 243 # this piece is for rounding the specific end of unknowns
242 244 b = bias.get(part)
243 245 if b is None:
244 246 if part[0:1] in b"HMS":
245 247 b = b"00"
246 248 else:
247 b = b"0"
249 # year, month, and day start from 1
250 b = b"1"
248 251
249 252 # this piece is for matching the generic end to today's date
250 253 n = datestr(now, b"%" + part[0:1])
251 254
252 255 defaults[part] = (b, n)
253 256
254 257 for format in formats:
255 258 try:
256 259 when, offset = strdate(date, format, defaults)
257 260 except (ValueError, OverflowError):
258 261 pass
259 262 else:
260 263 break
261 264 else:
262 265 raise error.ParseError(
263 266 _(b'invalid date: %r') % pycompat.bytestr(date)
264 267 )
265 268 # validate explicit (probably user-specified) date and
266 269 # time zone offset. values must fit in signed 32 bits for
267 270 # current 32-bit linux runtimes. timezones go from UTC-12
268 271 # to UTC+14
269 272 if when < -0x80000000 or when > 0x7FFFFFFF:
270 273 raise error.ParseError(_(b'date exceeds 32 bits: %d') % when)
271 274 if offset < -50400 or offset > 43200:
272 275 raise error.ParseError(_(b'impossible time zone offset: %d') % offset)
273 276 return when, offset
274 277
275 278
276 279 def matchdate(date):
277 280 """Return a function that matches a given date match specifier
278 281
279 282 Formats include:
280 283
281 284 '{date}' match a given date to the accuracy provided
282 285
283 286 '<{date}' on or before a given date
284 287
285 288 '>{date}' on or after a given date
286 289
287 290 >>> p1 = parsedate(b"10:29:59")
288 291 >>> p2 = parsedate(b"10:30:00")
289 292 >>> p3 = parsedate(b"10:30:59")
290 293 >>> p4 = parsedate(b"10:31:00")
291 294 >>> p5 = parsedate(b"Sep 15 10:30:00 1999")
292 295 >>> f = matchdate(b"10:30")
293 296 >>> f(p1[0])
294 297 False
295 298 >>> f(p2[0])
296 299 True
297 300 >>> f(p3[0])
298 301 True
299 302 >>> f(p4[0])
300 303 False
301 304 >>> f(p5[0])
302 305 False
303 306 """
304 307
305 308 def lower(date):
306 309 d = {b'mb': b"1", b'd': b"1"}
307 310 return parsedate(date, extendeddateformats, d)[0]
308 311
309 312 def upper(date):
310 313 d = {b'mb': b"12", b'HI': b"23", b'M': b"59", b'S': b"59"}
311 314 for days in (b"31", b"30", b"29"):
312 315 try:
313 316 d[b"d"] = days
314 317 return parsedate(date, extendeddateformats, d)[0]
315 318 except error.ParseError:
316 319 pass
317 320 d[b"d"] = b"28"
318 321 return parsedate(date, extendeddateformats, d)[0]
319 322
320 323 date = date.strip()
321 324
322 325 if not date:
323 326 raise error.Abort(_(b"dates cannot consist entirely of whitespace"))
324 327 elif date[0:1] == b"<":
325 328 if not date[1:]:
326 329 raise error.Abort(_(b"invalid day spec, use '<DATE'"))
327 330 when = upper(date[1:])
328 331 return lambda x: x <= when
329 332 elif date[0:1] == b">":
330 333 if not date[1:]:
331 334 raise error.Abort(_(b"invalid day spec, use '>DATE'"))
332 335 when = lower(date[1:])
333 336 return lambda x: x >= when
334 337 elif date[0:1] == b"-":
335 338 try:
336 339 days = int(date[1:])
337 340 except ValueError:
338 341 raise error.Abort(_(b"invalid day spec: %s") % date[1:])
339 342 if days < 0:
340 343 raise error.Abort(
341 344 _(b"%s must be nonnegative (see 'hg help dates')") % date[1:]
342 345 )
343 346 when = makedate()[0] - days * 3600 * 24
344 347 return lambda x: x >= when
345 348 elif b" to " in date:
346 349 a, b = date.split(b" to ")
347 350 start, stop = lower(a), upper(b)
348 351 return lambda x: x >= start and x <= stop
349 352 else:
350 353 start, stop = lower(date), upper(date)
351 354 return lambda x: x >= start and x <= stop
General Comments 0
You need to be logged in to leave comments. Login now