From patchwork Sun Oct 9 12:53:25 2016 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: [v2] templater: provide arithmetic operations on integers From: Simon Farnsworth X-Patchwork-Id: 16990 Message-Id: <2e2c959de0fe2c17bf6c.1476017605@devvm022.lla2.facebook.com> To: Date: Sun, 9 Oct 2016 05:53:25 -0700 # HG changeset patch # User Simon Farnsworth # Date 1476017464 25200 # Sun Oct 09 05:51:04 2016 -0700 # Node ID 2e2c959de0fe2c17bf6c5f47c01035a36f13c593 # Parent dbcef8918bbdd8a64d9f79a37bcfa284a26f3a39 templater: provide arithmetic operations on integers The termwidth template keyword is of limited use without some way to ensure that margins are respected. Provide a full set of arithmetic operators (four basic operations plus the mod function, defined to match Python's // for division), so that you can create termwidth based layouts that match the user's terminal size diff --git a/mercurial/help/templates.txt b/mercurial/help/templates.txt --- a/mercurial/help/templates.txt +++ b/mercurial/help/templates.txt @@ -43,6 +43,12 @@ .. functionsmarker +We provide a limited set of infix arithmetic operations on integers: + + for addition + - for subtraction + * for multiplication + / for floor division (division rounded to integer nearest -infinity) +Division fulfils the law x = x / y + mod(x, y). Also, for any expression that returns a list, there is a list operator:: expr % "{template}" diff --git a/mercurial/templater.py b/mercurial/templater.py --- a/mercurial/templater.py +++ b/mercurial/templater.py @@ -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('revset(query[, formatargs...])') def revset(context, mapping, args): """Execute a revision set query. See @@ -906,6 +941,7 @@ # methods to interpret function arguments or inner expressions (e.g. {_(x)}) exprmethods = { "integer": lambda e, c: (runinteger, e[1]), + "negate": lambda e, c: (runinteger, e[1]), "string": lambda e, c: (runstring, e[1]), "symbol": lambda e, c: (runsymbol, e[1]), "template": buildtemplate, @@ -914,6 +950,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"}) diff --git a/tests/test-command-template.t b/tests/test-command-template.t --- a/tests/test-command-template.t +++ b/tests/test-command-template.t @@ -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):