如何测试提供给expl3
函数的参数是否为整数表达式?如果不是,并且我调用任何整数相关函数,则会产生错误 ( ! Missing number, treated as zero.
)。我想在生成 TeX 错误之前捕获此错误。
平均能量损失
\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}
答案1
1 检查数字是否为整数
可能\int_eval
并且\fp_eval
将来会支持您要求的功能:
如果人们认为它有用,我可能会调整
l3fp
解析器以提供一个函数来“解析直到失败”并返回拆分标记列表、结果和真/假。它可以有int
、、、变体。 — Bruno Le Flochdim
skip
fp
GitHub
同时,您可以使用正则表达式检查正则整数。如果您想知道此正则表达式是否可以扩展以涵盖可以\int_eval
消化的表达式,那么我不得不让您失望了。正则表达式不支持递归,并且如您所见,(1+(1+(1+1)))
例如是递归的。
\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}
输出结果没有什么意外。
2 检查表达式是否可以计算为整数
如果您想知道表达式是否会在 中解析\int_eval
,则可以使用 LPEG(Lua 解析表达式语法)来解析数学表达式。 优点是它可以完全扩展。 另一方面,这完全没有意义,因为无论如何您都可以在 Lua 中评估表达式。
下面的三个片段是一个连续的文件,但除此之外,TeX.SX 不允许我混合语法突出显示。(感谢@Mico 的想法!)
\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}
3 实现自己的整数计算器
既然我们已经超越了合理的范围,为什么不更进一步,用 Lua 实现一个完整的数学表达式解析器和求值器呢?
这个东西是基于我更完整的 Lua 浮点数数学解析器,可以在这里找到我的 GitHub 上的 Gist(这个有更多花哨的功能,并支持数学常数和函数)。
\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}
答案2
不久前我写了一个程序来做这件事。这是测试 \numexpr#1\relax 是否会用尽所有输入,例如,如果数字中间有空格,它将返回 false。
它没有捕获-(1)
。另一件事是它不处理寄存器。如果你们还有其他违反直觉的情况(或返回错误答案的情况),那么我很乐意听听你们的意见。
该实现是一个有限状态机,带有一个额外的计数器来跟踪括号。最棘手的部分是正确处理宏的扩展。
\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}