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