7

How can I test if an argument supplied to an expl3 function is an integer expression? If it's not and I call any integer related function an error is generated (! Missing number, treated as zero.). I'd like to catch this before the TeX error is generated.

MWE

\documentclass{article}
\usepackage{expl3}
\begin{document}

\ExplSyntaxOn

\prg_new_protected_conditional:Npnn \if_is_int:n #1 { T, F, TF }
  {
    \int_eval:n { #1 }
    :~
    \prg_return_true:
  }

\if_is_int:nTF { 1 }
  { Is~integer }
  { Is~not~integer }

\par

\if_is_int:nTF { \c_one + 1 }
  { Is~integer }
  { Is~not~integer }

\par

\if_is_int:nTF { String }
  { Is~integer }
  { Is~not~integer }

\ExplSyntaxOff

\end{document}
3
  • 1
    do you mean "integer" or "integer expression" i.e. "3*(5-7^8)" (assuming here fpeval like syntax allowed, so with ^) or perhaps \macroA\macroB\macroC\macroD where each \macroX will expand to a part of the expression?
    – user4686
    Commented Apr 20, 2018 at 7:07
  • @jfbu, at least integer, but if possible something that \int_eval:n accepts as an integer. Commented Apr 20, 2018 at 7:10
  • in latter case that means the full parser force is involved. This is much more complicated in TeX than say in a syntax checker of a CAS, due to expansion.
    – user4686
    Commented Apr 20, 2018 at 7:21

2 Answers 2

10

1 Check whether a number is an integer

Maybe \int_eval and \fp_eval will support the feature you ask for in the future:

If people think it's useful I could probably adapt the l3fp parser to provide a function to "parse until failure" and return the split token list, the result, and true/false. It could have an int, dim, skip, fp variants. — Bruno Le Floch on GitHub

Meanwhile you can check for a regular integer using a regular expression. In case you are wondering whether this regex could be extended to cover an expression that \int_eval can digest, I have to disappoint you. Regular expressions do not support recursion and, as you can see, (1+(1+(1+1))) for example is recursive.

\documentclass{article}
\usepackage{expl3}
\begin{document}

\ExplSyntaxOn

\prg_new_protected_conditional:Npnn \if_is_int:n #1 { T, F, TF }
  {
    \regex_match:nnTF { ^[\+\-]?[\d]+$ } {#1} % $
      { \prg_return_true: }
      { \prg_return_false: }
  }

\if_is_int:nTF { 1 }
  { Is~integer }
  { Is~not~integer }

\par

\if_is_int:nTF { String }
  { Is~integer }
  { Is~not~integer }

\ExplSyntaxOff

\end{document}

No surprises in the output.

enter image description here


2 Check whether an expression can evaluate to an integer

If you are keen on knowing whether the expression will parse in \int_eval, you could use LPEG (Lua Parsing Expression Grammar) to parse the mathematical expression. The advantage is that this is fully expandable. On the other hand it is completely pointless because then you could just evaluate the expression in Lua anyway.

The three snippets below are one continuous file but otherwise TeX.SX wouldn't let me mix syntax highlighting. (Thanks @Mico for the idea!)

\documentclass{article}
\pagestyle{empty}
\usepackage{expl3}
\usepackage{luacode}
\begin{document}

\begin{luacode*}
local lpeg = require"lpeg"
local P, R, S, V = lpeg.P, lpeg.R, lpeg.S, lpeg.V

local white = S(" \t") ^ 0

local integer  = white * R("09") ^ 1 * white
local exponent = white * P("^") * white
local muldiv   = white * S("/*") * white
local addsub   = white * S("+-") * white
local open     = white * P("(") * white
local close    = white * P(")") * white

local calculator = P({
  "input",
  input      = V("expression") * -1,
  expression = V("term") * (addsub * V("term"))^0,
  term       = V("primary") * (muldiv * V("primary"))^0,
  primary    = integer + ( open * V("expression") * close )
})

function check_integer(str)
   if calculator:match(str) then
      tex.print("\\prg_return_true:")
   else
      tex.print("\\prg_return_false:")
   end
end
\end{luacode*}

\ExplSyntaxOn

\prg_new_conditional:Npnn \if_is_int:n #1 { p, T, F, TF }
  {
    \lua_now_x:n { check_integer([[\lua_escape_x:n {#1}]]) }
  }

\if_is_int:nTF { 5*3+2 }      { Is~integer } { Is~not~integer }\par
\if_is_int:nTF { 3*(5-7^8) }  { Is~integer } { Is~not~integer }\par
\if_is_int:nTF { -(1) }       { Is~integer } { Is~not~integer }\par
\if_is_int:nTF { String }     { Is~integer } { Is~not~integer }\par

\ExplSyntaxOff

\end{document}

enter image description here


3 Implement your own integer calculator

Since we have long gone beyond what is sensible to do, why not go the extra mile and implement a complete mathematical expression parser and evaluator in Lua.

The thing is based off of my even more complete math parser for floating point numbers in Lua which can be found as a Gist on my GitHub (This one has even more bells and whistles and supports mathematical constants and functions).

\documentclass{article}
\usepackage{expl3}
\usepackage{luacode}
\begin{document}

\begin{luacode*}
local lpeg = require"lpeg"
local C, P, R, S, V = lpeg.C, lpeg.P, lpeg.R, lpeg.S, lpeg.V

local white = S(" \t") ^ 0

local integer = white * C(R("09") ^ 1) * white / tonumber
local power   = white * C(P("^")) * white
local muldiv  = white * C(S("/*%")) * white
local addsub  = white * C(S("+-")) * white
local open    = white * P("(") * white
local close   = white * P(")") * white

-- Evaluate AST recursively
local function eval(t)
    if type(t) == "table" then
        if     t.op == "+" then return (t.left and eval(t.left) or 0) + eval(t.right)
        elseif t.op == "-" then return (t.left and eval(t.left) or 0) - eval(t.right)
        elseif t.op == "*" then return eval(t.left) * eval(t.right)
        -- Below is my poor excuse for missing integer division before Lua 5.3
        elseif t.op == "/" then return math.floor(eval(t.left) / eval(t.right))
        elseif t.op == "%" then return eval(t.left) % eval(t.right)
        elseif t.op == "^" then return eval(t.left) ^ eval(t.right)
        else error("Cannot happen") end
    elseif type(t) == "number" then
        return t
    else error("Cannot happen") end
end

-- Insert binary node into AST
local function binary(rule)
    local function recurse(left,op,right,...)
        if op then
            return recurse({ op = op, left = left, right = right },...)
        else
            return left
        end
    end
    return rule / recurse
end

-- Insert unary node into AST
local function unary(rule)
    return rule / function(op,right)
        return { op = op, right = right }
    end
end

local grammar = P({
        "input",
        input      = V("expression") * -1,
        expression = binary( V("term") * ( addsub * V("term") )^0 ),
        term       = binary( V("factor") * ( muldiv * V("factor"))^0 ),
        factor     = binary( V("primary") * ( power * V("factor"))^0 ),
        primary    = integer
            + open * V("expression") * close
            + unary( addsub * V("primary") )
})

int = {
    check = function(str)
        if grammar:match(str) then
            tex.print("\\prg_return_true:")
        else
            tex.print("\\prg_return_false:")
        end
    end,
    eval = function(str)
        local result = eval(assert(grammar:match(str)))
        tex.print(string.format("%d",result))
    end
}
\end{luacode*}

\ExplSyntaxOn

\prg_new_conditional:Npnn \if_lua_is_int:n #1 { p, T, F, TF }
  {
    \lua_now_x:n { int.check([[\lua_escape_x:n {#1}]]) }
  }

\cs_new:Npn \lua_int_eval:n #1
  {
    \lua_now_x:n { int.eval([[\lua_escape_x:n {#1}]]) }
  }

\if_lua_is_int:nTF { 5*3+2 }     { \lua_int_eval:n { 5*3+2 }     } { :( }\par
\if_lua_is_int:nTF { 3*(5-7^8) } { \lua_int_eval:n { 3*(5-7^8) } } { :( }\par
\if_lua_is_int:nTF { -(1) }      { \lua_int_eval:n { -(1) }      } { :( }\par
\if_lua_is_int:nTF { String }    { \lua_int_eval:n { String }    } { :( }\par

\ExplSyntaxOff

\end{document}

enter image description here

22
  • Thanks. That might be good enough for my purposes. The GitHub link is helpful too. I take it there no easy way to deal with the general case where any macro or expression is specified (i.e., like @jfbu asked in the question comments)? Commented Apr 20, 2018 at 7:37
  • @DavidPurton I updated the expression parsing grammar in the Lua part. It can now parse a full expression including addition, subtraction, exponentials, and subexpressions. The next step would be to construct an abstract syntax tree (AST) and evaluate it recursively to get the result of the math expression, but as I said, this is kind of pointless. Commented Apr 20, 2018 at 7:53
  • OK, looks like my best options are either to stick with a regex test or put up with the ! Missing number, treated as zero error. Commented Apr 20, 2018 at 8:13
  • 1
    @jfbu That is true, but to be honest, I don't want to reproduce all the parsing oddities of \numexpr (especially spaces in numbers, I mean, come on, who does that?) Commented Apr 21, 2018 at 0:57
  • 1
    I hope this won't appear as harassment ;-) in Part 2, I find it odd and unexpected that \if_is_int:nTF { 123/-45 } takes the False branch, as I pointed out in previous comment
    – user4686
    Commented Apr 21, 2018 at 10:26
4

I wrote a thing to do this a while back. This is testing to see whether \numexpr#1\relax would use up all of the input, so for instance if there are spaces in the middle of a number it will return false.

It doesn't catch -(1). Another thing is that it doesn't deal with registers. If you folks have other counterintuitive cases (or cases where this returns the wrong answer), then I would be happy to hear about them.

The implementation is a finite state machine with an extra counter to keep track of parentheses. The most tricky part is dealing with expansion of macros correctly.

\documentclass{article}
\usepackage{expl3}
\usepackage{xcolor}


\ExplSyntaxOn
\makeatletter
\let\@xp\expandafter
\let\@nx\noexpand
\newcount\ifintexpr@tempcount

% Test if single token input is a digit
\def\ifintexpr@ifdigit#1{
    \ifodd0
        \ifx#1 0 1 \else
        \ifx#1 1 1 \else
        \ifx#1 2 1 \else
        \ifx#1 3 1 \else
        \ifx#1 4 1 \else
        \ifx#1 5 1 \else
        \ifx#1 6 1 \else
        \ifx#1 7 1 \else
        \ifx#1 8 1 \else
        \ifx#1 9 1 \else
        \fi \fi \fi \fi \fi
        \fi \fi \fi \fi \fi
    \relax
        \@xp\@firstoftwo
    \else
        \@xp\@secondoftwo
    \fi
}

%%% ifintexpr
% #1 -- expression to test
% #2 -- true case
% #3 -- false dcase
% This tests true if \numexpr #1\relax throws no error and consumes all of #1 and false otherwise.
\def\ifintexpr#1{%
    \bgroup
    \ifintexpr@tempcount\z@
    \ifintexpr@{needsint}#1\ifintexpr@fexpsafenil%
    \@xp\egroup\next
}

\def\ifintexpr@fexpsafenil{\@nx\ifintexpr@fexpsafenil}

% We need to use \futurelet so that we can detect open braces even when they only surround one token like {1}
% also we use it to detect spaces. Store the state in \ifintexpr@state first.
\def\ifintexpr@#1{\def\ifintexpr@state{#1}\futurelet\testtok\ifintexpr@@}
\def\ifintexpr@@{%
    \ifx\testtok\bgroup%
        \let\next\ifintexpr@false
    \else
        \ifx\testtok\ifintexpr@fexpsafenil
            \@xp\let\@xp\next\csname sseq@ifintexpr@@\ifintexpr@state @done\endcsname
        \else
            % We need to check here for a space because \string<space> produces NO OUTPUT regardless of the catcode of the space.
            % This messes up \ifintexpr@@@ because it doesn't expect \string#1 to produce no characters.
            \@xp\ifx\space\testtok
                \def\next{\ifintexpr@next{\space}\@xp\next\romannumeral-`0}
            \else
                \let\next\ifintexpr@@@
            \fi
        \fi
    \fi
    \next
}

\def\ifintexpr@@@#1{
    \ifcat$\@xp\@gobble\string#1$%
        \@xp\ifintexpr@@@@\@xp#1
    \else
        % This is a macro, so fexpand it
        % Then use f expansion.
        \@xp\ifintexpr@@@fexpcs\@xp#1
    \fi
}

\def\ifintexpr@@@fexpcs{\exp_last_unbraced:Nf\ifintexpr@@@fexpcs@}
\def\ifintexpr@@@fexpcs@{\futurelet\testtok\ifintexpr@@@fexpcs@@}
\def\ifintexpr@@@fexpcs@@{
    \ifx\testtok\bgroup
        \@xp\ifintexpr@false % We already tested for groups above, so we need to check if this expanded to a group
    \else
        \@xp\ifintexpr@@@@ % If it's still a control sequence, then this will fail in the \@ifundefined step
    \fi
}



% We can't just use \futurelet because "\let\testtok(" makes \testtok unexpandable
% (I guess that makes sense, but why is it that I need \@xp\ifx\otherspace above if I've also \let\otherspace to a character? Mysterious...),
% so then "\csname hello\testtok\endcsname" is an error. This indexes into our state machine,
% cases: a digits, + or -, * or /, (, ), or something else (anything else always leads to false
\def\ifintexpr@@@@#1{%
    \ifx#1\ifintexpr@fexpsafenil
        \def\next{\csname sseq@ifintexpr@@\ifintexpr@state @done\endcsname\ifintexpr@fexpsafenil}%
    \else
        \ifintexpr@ifdigit{#1}%
            {\ifintexpr@next{digit}}%
            {%
                \ifx#1+%
                    \ifintexpr@next{+-}
                \else
                    \ifx#1-%
                        \ifintexpr@next{+-}
                    \else
                        \ifx#1*%
                            \ifintexpr@next{*/}
                        \else
                            \ifx#1/%
                                \ifintexpr@next{*/}
                            \else
                                % This extra \string here is so that if a control sequence fexpanded and still gave a control sequence,
                                % we don't get a missing \endcsname error here, it just returns false
                                \@ifundefined{sseq@ifintexpr@@\ifintexpr@state @\string#1}%
                                    {\let\next\ifintexpr@false}%
                                    {\ifintexpr@next{#1}}%
                            \fi
                        \fi
                    \fi
                \fi
            }%
    \fi
    \next
}

\def\ifintexpr@true#1\ifintexpr@fexpsafenil{\ifnum\ifintexpr@tempcount=\z@ \let\next\@firstoftwo\else\let\next\@secondoftwo\fi}
\def\ifintexpr@false#1\ifintexpr@fexpsafenil{\let\next\@secondoftwo}

\def\ifintexpr@makeifint#1#2#3{\@xp\def\csname sseq@ifintexpr@@#1@#2\endcsname{#3}}
\def\ifintexpr@next#1{\@xp\let\@xp\next\csname sseq@ifintexpr@@\ifintexpr@state @#1\endcsname}

\ifintexpr@makeifint{needsint}{done}{\ifintexpr@false}
\ifintexpr@makeifint{needsint}{digit}{\ifintexpr@{int}}
\ifintexpr@makeifint{needsint}{*/}{\ifintexpr@false}
\ifintexpr@makeifint{needsint}{+-}{\ifintexpr@{needsint}}
\ifintexpr@makeifint{needsint}{(}{\advance\ifintexpr@tempcount\@ne\ifintexpr@{needsint}}
\ifintexpr@makeifint{needsint}{)}{\ifintexpr@false}
\ifintexpr@makeifint{needsint}{\space}{\ifintexpr@{needsint}}

\ifintexpr@makeifint{int}{done}{\ifintexpr@true}
\ifintexpr@makeifint{int}{digit}{\ifintexpr@{int}}
\ifintexpr@makeifint{int}{*/}{\ifintexpr@{needsint}}
\ifintexpr@makeifint{int}{+-}{\ifintexpr@{needsint}}
\ifintexpr@makeifint{int}{(}{\ifintexpr@false}
\ifintexpr@makeifint{int}{)}{
    \advance\ifintexpr@tempcount\m@ne
    \ifnum\ifintexpr@tempcount<\z@\relax
        \@xp\@xp\@xp\ifintexpr@false\@xp\@gobble
    \else
        \@xp\ifintexpr@
    \fi{nointallowed}
}
\ifintexpr@makeifint{int}{\space}{\ifintexpr@{nointallowed}}

\ifintexpr@makeifint{nointallowed}{done}{\ifintexpr@true}
\ifintexpr@makeifint{nointallowed}{digit}{\ifintexpr@false}
\ifintexpr@makeifint{nointallowed}{*/}{\ifintexpr@{needsint}}
\ifintexpr@makeifint{nointallowed}{+-}{\ifintexpr@{needsint}}
\ifintexpr@makeifint{nointallowed}{(}{\ifintexpr@false}
\ifintexpr@makeifint{nointallowed}{)}{
    \advance\ifintexpr@tempcount\m@ne
    \ifnum\ifintexpr@tempcount<\z@
        \@xp\@xp\@xp\ifintexpr@false\@xp\@gobble
    \else
        \@xp\ifintexpr@
    \fi{nointallowed}
}
\ifintexpr@makeifint{nointallowed}{\space}{\ifintexpr@{nointallowed}}

\ExplSyntaxOff
\makeatother

\begin{document}
\def\testifintexpr#1{\bgroup\ifintexpr{#1}{\color{blue}}{\color{red}}\texttt{\detokenize{#1}}\egroup\par}

\testifintexpr{-(1)} % Passes but apparently shouldn't ?!
\testifintexpr{1+---+++2}  % Passes
\testifintexpr{1*+---+++2} % Fails
\testifintexpr{1 1+-2}     % Fail
\testifintexpr{1+(2*3-1}   % Fail
\testifintexpr{1+(2*3-1)}  % Pass
\testifintexpr{2**3}       % Fail
\newcount\test\texttt{\detokenize{\newcount\test}}\par
\testifintexpr{\test}      % Fail even though \numexpr would pass
\def\testb{2+}\texttt{\detokenize{\def\testb{2+}}}\par
\testifintexpr{\testb1}
\testifintexpr{\testb}
\end{document}
4
  • You can study the parsing code scan_expr() in scanning.w if you really want to find all the corner cases. Commented Apr 21, 2018 at 4:18
  • 2
    +1 but I am very disturbed with \ExplSyntaxOn/\ExplSyntaxOff with traditional TeX code ; as a result it is very difficult to read the TeX because one must keep in mind spaces do not exist, for example the \ifodd0 <etc> in \ifintexpr@ifdigit is very disturbing to root conservative types.
    – user4686
    Commented Apr 21, 2018 at 7:16
  • If this disturbs people I could refactor it not to use expl3, since the only macro I'm getting from it is \exp_last_unbraced:Nf which is just \expandafter\somemacro\romannumeral-`0, but it's such a relief not to have to worry about dropping a % somewhere... Commented Apr 21, 2018 at 11:22
  • of course that's your call :-). Some people put % everywhere... I find that original TeX makes play such a special roles to spaces, particularly when dealing with integers, (and this has been taken over in \numexpr) that killing them all but still using \if, \ifnum tests is unsettling. But if it has proved useful to you, of course I can not argue ;-).
    – user4686
    Commented Apr 21, 2018 at 20:30

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .