--- a/mercurial/templater.py Sun Oct 09 15:54:42 2016 +0200
+++ b/mercurial/templater.py Sun Oct 09 05:51:04 2016 -0700
@@ -33,6 +33,10 @@
"|": (5, None, None, ("|", 5), None),
"%": (6, None, None, ("%", 6), None),
")": (0, None, None, None, None),
+ "+": (3, None, None, ("+", 3), None),
+ "-": (3, None, ("negate", 10), ("-", 3), None),
+ "*": (4, None, None, ("*", 4), None),
+ "/": (4, None, None, ("/", 4), None),
"integer": (0, "integer", None, None, None),
"symbol": (0, "symbol", None, None, None),
"string": (0, "string", None, None, None),
@@ -48,7 +52,7 @@
c = program[pos]
if c.isspace(): # skip inter-token whitespace
pass
- elif c in "(,)%|": # handle simple operators
+ elif c in "(,)%|+-*/": # handle simple operators
yield (c, None, pos)
elif c in '"\'': # handle quoted templates
s = pos + 1
@@ -70,13 +74,8 @@
pos += 1
else:
raise error.ParseError(_("unterminated string"), s)
- elif c.isdigit() or c == '-':
+ elif c.isdigit():
s = pos
- if c == '-': # simply take negate operator as part of integer
- pos += 1
- if pos >= end or not program[pos].isdigit():
- raise error.ParseError(_("integer literal without digits"), s)
- pos += 1
while pos < end:
d = program[pos]
if not d.isdigit():
@@ -420,6 +419,28 @@
# If so, return the expanded value.
yield i
+def buildnegate(exp, context):
+ arg = compileexp(exp[1], context, exprmethods)
+ return (runnegate, arg)
+
+def runnegate(context, mapping, data):
+ data = evalinteger(context, mapping, data,
+ _('negation needs an integer argument'))
+ return -data
+
+def buildarithmetic(exp, context, func):
+ left = compileexp(exp[1], context, exprmethods)
+ right = compileexp(exp[2], context, exprmethods)
+ return (runarithmetic, (func, left, right))
+
+def runarithmetic(context, mapping, data):
+ func, left, right = data
+ left = evalinteger(context, mapping, left,
+ _('arithmetic only defined on integers'))
+ right = evalinteger(context, mapping, right,
+ _('arithmetic only defined on integers'))
+ return func(left, right)
+
def buildfunc(exp, context):
n = getsymbol(exp[1])
args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
@@ -713,6 +734,20 @@
tzoffset = util.makedate()[1]
return (date[0], tzoffset)
+@templatefunc('mod(a, b)')
+def mod(context, mapping, args):
+ """Calculate a mod b such that a / b + a mod b == a"""
+ if not len(args) == 2:
+ # i18n: "mod" is a keyword
+ raise error.ParseError(_("mod expects two arguments"))
+
+ left = evalinteger(context, mapping, args[0],
+ _('arithmetic only defined on integers'))
+ right = evalinteger(context, mapping, args[1],
+ _('arithmetic only defined on integers'))
+
+ return left % right
+
@templatefunc('relpath(path)')
def relpath(context, mapping, args):
"""Convert a repository-absolute path into a filesystem path relative to
@@ -926,6 +961,11 @@
"|": buildfilter,
"%": buildmap,
"func": buildfunc,
+ "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
+ "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
+ "negate": buildnegate,
+ "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
+ "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
}
# methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
--- a/tests/test-command-template.t Sun Oct 09 15:54:42 2016 +0200
+++ b/tests/test-command-template.t Sun Oct 09 05:51:04 2016 -0700
@@ -29,6 +29,111 @@
$ hg merge -q foo
$ hg commit -m 'merge' -d '1500001 0' -u 'person'
+Test arithmetic operators have the right precedence:
+
+ $ hg log -l 1 -T '{date(date, "%s") + 5 * 10} {date(date, "%s") - 2 * 3}\n'
+ 1500051 1499995
+ $ hg log -l 1 -T '{date(date, "%s") * 5 + 10} {date(date, "%s") * 3 - 2}\n'
+ 7500015 4500001
+
+Test division:
+
+ $ hg debugtemplate -r0 -v '{5 / 2} {mod(5, 2)}\n'
+ (template
+ (/
+ ('integer', '5')
+ ('integer', '2'))
+ ('string', ' ')
+ (func
+ ('symbol', 'mod')
+ (list
+ ('integer', '5')
+ ('integer', '2')))
+ ('string', '\n'))
+ 2 1
+ $ hg debugtemplate -r0 -v '{5 / -2} {mod(5, -2)}\n'
+ (template
+ (/
+ ('integer', '5')
+ (negate
+ ('integer', '2')))
+ ('string', ' ')
+ (func
+ ('symbol', 'mod')
+ (list
+ ('integer', '5')
+ (negate
+ ('integer', '2'))))
+ ('string', '\n'))
+ -3 -1
+ $ hg debugtemplate -r0 -v '{-5 / 2} {mod(-5, 2)}\n'
+ (template
+ (/
+ (negate
+ ('integer', '5'))
+ ('integer', '2'))
+ ('string', ' ')
+ (func
+ ('symbol', 'mod')
+ (list
+ (negate
+ ('integer', '5'))
+ ('integer', '2')))
+ ('string', '\n'))
+ -3 1
+ $ hg debugtemplate -r0 -v '{-5 / -2} {mod(-5, -2)}\n'
+ (template
+ (/
+ (negate
+ ('integer', '5'))
+ (negate
+ ('integer', '2')))
+ ('string', ' ')
+ (func
+ ('symbol', 'mod')
+ (list
+ (negate
+ ('integer', '5'))
+ (negate
+ ('integer', '2'))))
+ ('string', '\n'))
+ 2 -1
+
+Filters bind closer than arithmetic:
+
+ $ hg debugtemplate -r0 -v '{revset(".")|count - 1}\n'
+ (template
+ (-
+ (|
+ (func
+ ('symbol', 'revset')
+ ('string', '.'))
+ ('symbol', 'count'))
+ ('integer', '1'))
+ ('string', '\n'))
+ 0
+
+But negate binds closer still:
+
+ $ hg debugtemplate -r0 -v '{1-3|stringify}\n'
+ (template
+ (-
+ ('integer', '1')
+ (|
+ ('integer', '3')
+ ('symbol', 'stringify')))
+ ('string', '\n'))
+ hg: parse error: arithmetic only defined on integers
+ [255]
+ $ hg debugtemplate -r0 -v '{-3|stringify}\n'
+ (template
+ (|
+ (negate
+ ('integer', '3'))
+ ('symbol', 'stringify'))
+ ('string', '\n'))
+ -3
+
Second branch starting at nullrev:
$ hg update null
@@ -2890,14 +2995,15 @@
$ hg debugtemplate -v '{(-4)}\n'
(template
(group
- ('integer', '-4'))
+ (negate
+ ('integer', '4')))
('string', '\n'))
-4
$ hg debugtemplate '{(-)}\n'
- hg: parse error at 2: integer literal without digits
+ hg: parse error at 3: not a prefix: )
[255]
$ hg debugtemplate '{(-a)}\n'
- hg: parse error at 2: integer literal without digits
+ hg: parse error: negation needs an integer argument
[255]
top-level integer literal is interpreted as symbol (i.e. variable name):