18 pycompat, |
18 pycompat, |
19 ) |
19 ) |
20 |
20 |
21 # used by parsedate |
21 # used by parsedate |
22 defaultdateformats = ( |
22 defaultdateformats = ( |
23 '%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601 |
23 '%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601 |
24 '%Y-%m-%dT%H:%M', # without seconds |
24 '%Y-%m-%dT%H:%M', # without seconds |
25 '%Y-%m-%dT%H%M%S', # another awful but legal variant without : |
25 '%Y-%m-%dT%H%M%S', # another awful but legal variant without : |
26 '%Y-%m-%dT%H%M', # without seconds |
26 '%Y-%m-%dT%H%M', # without seconds |
27 '%Y-%m-%d %H:%M:%S', # our common legal variant |
27 '%Y-%m-%d %H:%M:%S', # our common legal variant |
28 '%Y-%m-%d %H:%M', # without seconds |
28 '%Y-%m-%d %H:%M', # without seconds |
29 '%Y-%m-%d %H%M%S', # without : |
29 '%Y-%m-%d %H%M%S', # without : |
30 '%Y-%m-%d %H%M', # without seconds |
30 '%Y-%m-%d %H%M', # without seconds |
31 '%Y-%m-%d %I:%M:%S%p', |
31 '%Y-%m-%d %I:%M:%S%p', |
32 '%Y-%m-%d %H:%M', |
32 '%Y-%m-%d %H:%M', |
33 '%Y-%m-%d %I:%M%p', |
33 '%Y-%m-%d %I:%M%p', |
34 '%Y-%m-%d', |
34 '%Y-%m-%d', |
35 '%m-%d', |
35 '%m-%d', |
36 '%m/%d', |
36 '%m/%d', |
37 '%m/%d/%y', |
37 '%m/%d/%y', |
38 '%m/%d/%Y', |
38 '%m/%d/%Y', |
39 '%a %b %d %H:%M:%S %Y', |
39 '%a %b %d %H:%M:%S %Y', |
40 '%a %b %d %I:%M:%S%p %Y', |
40 '%a %b %d %I:%M:%S%p %Y', |
41 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822" |
41 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822" |
42 '%b %d %H:%M:%S %Y', |
42 '%b %d %H:%M:%S %Y', |
43 '%b %d %I:%M:%S%p %Y', |
43 '%b %d %I:%M:%S%p %Y', |
44 '%b %d %H:%M:%S', |
44 '%b %d %H:%M:%S', |
45 '%b %d %I:%M:%S%p', |
45 '%b %d %I:%M:%S%p', |
46 '%b %d %H:%M', |
46 '%b %d %H:%M', |
51 '%I:%M:%S%p', |
51 '%I:%M:%S%p', |
52 '%H:%M', |
52 '%H:%M', |
53 '%I:%M%p', |
53 '%I:%M%p', |
54 ) |
54 ) |
55 |
55 |
56 extendeddateformats = defaultdateformats + ( |
56 extendeddateformats = defaultdateformats + ("%Y", "%Y-%m", "%b", "%b %Y",) |
57 "%Y", |
57 |
58 "%Y-%m", |
|
59 "%b", |
|
60 "%b %Y", |
|
61 ) |
|
62 |
58 |
63 def makedate(timestamp=None): |
59 def makedate(timestamp=None): |
64 '''Return a unix timestamp (or the current time) as a (unixtime, |
60 '''Return a unix timestamp (or the current time) as a (unixtime, |
65 offset) tuple based off the local timezone.''' |
61 offset) tuple based off the local timezone.''' |
66 if timestamp is None: |
62 if timestamp is None: |
67 timestamp = time.time() |
63 timestamp = time.time() |
68 if timestamp < 0: |
64 if timestamp < 0: |
69 hint = _("check your clock") |
65 hint = _("check your clock") |
70 raise error.Abort(_("negative timestamp: %d") % timestamp, hint=hint) |
66 raise error.Abort(_("negative timestamp: %d") % timestamp, hint=hint) |
71 delta = (datetime.datetime.utcfromtimestamp(timestamp) - |
67 delta = datetime.datetime.utcfromtimestamp( |
72 datetime.datetime.fromtimestamp(timestamp)) |
68 timestamp |
|
69 ) - datetime.datetime.fromtimestamp(timestamp) |
73 tz = delta.days * 86400 + delta.seconds |
70 tz = delta.days * 86400 + delta.seconds |
74 return timestamp, tz |
71 return timestamp, tz |
|
72 |
75 |
73 |
76 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'): |
74 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'): |
77 """represent a (unixtime, offset) tuple as a localized time. |
75 """represent a (unixtime, offset) tuple as a localized time. |
78 unixtime is seconds since the epoch, and offset is the time zone's |
76 unixtime is seconds since the epoch, and offset is the time zone's |
79 number of seconds away from UTC. |
77 number of seconds away from UTC. |
96 q, r = divmod(minutes, 60) |
94 q, r = divmod(minutes, 60) |
97 format = format.replace("%z", "%1%2") |
95 format = format.replace("%z", "%1%2") |
98 format = format.replace("%1", "%c%02d" % (sign, q)) |
96 format = format.replace("%1", "%c%02d" % (sign, q)) |
99 format = format.replace("%2", "%02d" % r) |
97 format = format.replace("%2", "%02d" % r) |
100 d = t - tz |
98 d = t - tz |
101 if d > 0x7fffffff: |
99 if d > 0x7FFFFFFF: |
102 d = 0x7fffffff |
100 d = 0x7FFFFFFF |
103 elif d < -0x80000000: |
101 elif d < -0x80000000: |
104 d = -0x80000000 |
102 d = -0x80000000 |
105 # Never use time.gmtime() and datetime.datetime.fromtimestamp() |
103 # Never use time.gmtime() and datetime.datetime.fromtimestamp() |
106 # because they use the gmtime() system call which is buggy on Windows |
104 # because they use the gmtime() system call which is buggy on Windows |
107 # for negative values. |
105 # for negative values. |
108 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d) |
106 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d) |
109 s = encoding.strtolocal(t.strftime(encoding.strfromlocal(format))) |
107 s = encoding.strtolocal(t.strftime(encoding.strfromlocal(format))) |
110 return s |
108 return s |
111 |
109 |
|
110 |
112 def shortdate(date=None): |
111 def shortdate(date=None): |
113 """turn (timestamp, tzoff) tuple into iso 8631 date.""" |
112 """turn (timestamp, tzoff) tuple into iso 8631 date.""" |
114 return datestr(date, format='%Y-%m-%d') |
113 return datestr(date, format='%Y-%m-%d') |
|
114 |
115 |
115 |
116 def parsetimezone(s): |
116 def parsetimezone(s): |
117 """find a trailing timezone, if any, in string, and return a |
117 """find a trailing timezone, if any, in string, and return a |
118 (offset, remainder) pair""" |
118 (offset, remainder) pair""" |
119 s = pycompat.bytestr(s) |
119 s = pycompat.bytestr(s) |
131 # ISO8601 trailing Z |
131 # ISO8601 trailing Z |
132 if s.endswith("Z") and s[-2:-1].isdigit(): |
132 if s.endswith("Z") and s[-2:-1].isdigit(): |
133 return 0, s[:-1] |
133 return 0, s[:-1] |
134 |
134 |
135 # ISO8601-style [+-]hh:mm |
135 # ISO8601-style [+-]hh:mm |
136 if (len(s) >= 6 and s[-6] in "+-" and s[-3] == ":" and |
136 if ( |
137 s[-5:-3].isdigit() and s[-2:].isdigit()): |
137 len(s) >= 6 |
|
138 and s[-6] in "+-" |
|
139 and s[-3] == ":" |
|
140 and s[-5:-3].isdigit() |
|
141 and s[-2:].isdigit() |
|
142 ): |
138 sign = (s[-6] == "+") and 1 or -1 |
143 sign = (s[-6] == "+") and 1 or -1 |
139 hours = int(s[-5:-3]) |
144 hours = int(s[-5:-3]) |
140 minutes = int(s[-2:]) |
145 minutes = int(s[-2:]) |
141 return -sign * (hours * 60 + minutes) * 60, s[:-6] |
146 return -sign * (hours * 60 + minutes) * 60, s[:-6] |
142 |
147 |
143 return None, s |
148 return None, s |
|
149 |
144 |
150 |
145 def strdate(string, format, defaults=None): |
151 def strdate(string, format, defaults=None): |
146 """parse a localized time string and return a (unixtime, offset) tuple. |
152 """parse a localized time string and return a (unixtime, offset) tuple. |
147 if the string cannot be parsed, ValueError is raised.""" |
153 if the string cannot be parsed, ValueError is raised.""" |
148 if defaults is None: |
154 if defaults is None: |
150 |
156 |
151 # NOTE: unixtime = localunixtime + offset |
157 # NOTE: unixtime = localunixtime + offset |
152 offset, date = parsetimezone(string) |
158 offset, date = parsetimezone(string) |
153 |
159 |
154 # add missing elements from defaults |
160 # add missing elements from defaults |
155 usenow = False # default to using biased defaults |
161 usenow = False # default to using biased defaults |
156 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity |
162 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity |
157 part = pycompat.bytestr(part) |
163 part = pycompat.bytestr(part) |
158 found = [True for p in part if ("%"+p) in format] |
164 found = [True for p in part if ("%" + p) in format] |
159 if not found: |
165 if not found: |
160 date += "@" + defaults[part][usenow] |
166 date += "@" + defaults[part][usenow] |
161 format += "@%" + part[0] |
167 format += "@%" + part[0] |
162 else: |
168 else: |
163 # We've found a specific time element, less specific time |
169 # We've found a specific time element, less specific time |
164 # elements are relative to today |
170 # elements are relative to today |
165 usenow = True |
171 usenow = True |
166 |
172 |
167 timetuple = time.strptime(encoding.strfromlocal(date), |
173 timetuple = time.strptime( |
168 encoding.strfromlocal(format)) |
174 encoding.strfromlocal(date), encoding.strfromlocal(format) |
|
175 ) |
169 localunixtime = int(calendar.timegm(timetuple)) |
176 localunixtime = int(calendar.timegm(timetuple)) |
170 if offset is None: |
177 if offset is None: |
171 # local timezone |
178 # local timezone |
172 unixtime = int(time.mktime(timetuple)) |
179 unixtime = int(time.mktime(timetuple)) |
173 offset = unixtime - localunixtime |
180 offset = unixtime - localunixtime |
174 else: |
181 else: |
175 unixtime = localunixtime + offset |
182 unixtime = localunixtime + offset |
176 return unixtime, offset |
183 return unixtime, offset |
|
184 |
177 |
185 |
178 def parsedate(date, formats=None, bias=None): |
186 def parsedate(date, formats=None, bias=None): |
179 """parse a localized date/time and return a (unixtime, offset) tuple. |
187 """parse a localized date/time and return a (unixtime, offset) tuple. |
180 |
188 |
181 The date may be a "unixtime offset" string or in one of the specified |
189 The date may be a "unixtime offset" string or in one of the specified |
209 return makedate() |
217 return makedate() |
210 if date == 'today' or date == _('today'): |
218 if date == 'today' or date == _('today'): |
211 date = datetime.date.today().strftime(r'%b %d') |
219 date = datetime.date.today().strftime(r'%b %d') |
212 date = encoding.strtolocal(date) |
220 date = encoding.strtolocal(date) |
213 elif date == 'yesterday' or date == _('yesterday'): |
221 elif date == 'yesterday' or date == _('yesterday'): |
214 date = (datetime.date.today() - |
222 date = (datetime.date.today() - datetime.timedelta(days=1)).strftime( |
215 datetime.timedelta(days=1)).strftime(r'%b %d') |
223 r'%b %d' |
|
224 ) |
216 date = encoding.strtolocal(date) |
225 date = encoding.strtolocal(date) |
217 |
226 |
218 try: |
227 try: |
219 when, offset = map(int, date.split(' ')) |
228 when, offset = map(int, date.split(' ')) |
220 except ValueError: |
229 except ValueError: |
242 pass |
251 pass |
243 else: |
252 else: |
244 break |
253 break |
245 else: |
254 else: |
246 raise error.ParseError( |
255 raise error.ParseError( |
247 _('invalid date: %r') % pycompat.bytestr(date)) |
256 _('invalid date: %r') % pycompat.bytestr(date) |
|
257 ) |
248 # validate explicit (probably user-specified) date and |
258 # validate explicit (probably user-specified) date and |
249 # time zone offset. values must fit in signed 32 bits for |
259 # time zone offset. values must fit in signed 32 bits for |
250 # current 32-bit linux runtimes. timezones go from UTC-12 |
260 # current 32-bit linux runtimes. timezones go from UTC-12 |
251 # to UTC+14 |
261 # to UTC+14 |
252 if when < -0x80000000 or when > 0x7fffffff: |
262 if when < -0x80000000 or when > 0x7FFFFFFF: |
253 raise error.ParseError(_('date exceeds 32 bits: %d') % when) |
263 raise error.ParseError(_('date exceeds 32 bits: %d') % when) |
254 if offset < -50400 or offset > 43200: |
264 if offset < -50400 or offset > 43200: |
255 raise error.ParseError(_('impossible time zone offset: %d') % offset) |
265 raise error.ParseError(_('impossible time zone offset: %d') % offset) |
256 return when, offset |
266 return when, offset |
|
267 |
257 |
268 |
258 def matchdate(date): |
269 def matchdate(date): |
259 """Return a function that matches a given date match specifier |
270 """Return a function that matches a given date match specifier |
260 |
271 |
261 Formats include: |
272 Formats include: |
317 try: |
328 try: |
318 days = int(date[1:]) |
329 days = int(date[1:]) |
319 except ValueError: |
330 except ValueError: |
320 raise error.Abort(_("invalid day spec: %s") % date[1:]) |
331 raise error.Abort(_("invalid day spec: %s") % date[1:]) |
321 if days < 0: |
332 if days < 0: |
322 raise error.Abort(_("%s must be nonnegative (see 'hg help dates')") |
333 raise error.Abort( |
323 % date[1:]) |
334 _("%s must be nonnegative (see 'hg help dates')") % date[1:] |
|
335 ) |
324 when = makedate()[0] - days * 3600 * 24 |
336 when = makedate()[0] - days * 3600 * 24 |
325 return lambda x: x >= when |
337 return lambda x: x >= when |
326 elif b" to " in date: |
338 elif b" to " in date: |
327 a, b = date.split(b" to ") |
339 a, b = date.split(b" to ") |
328 start, stop = lower(a), upper(b) |
340 start, stop = lower(a), upper(b) |