mercurial/utils/dateutil.py
changeset 43076 2372284d9457
parent 40256 d4d2c567bb72
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    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)