完全可扩展的循环宏,也适用于表格

完全可扩展的循环宏,也适用于表格

由于我通常tikz在文档中使用,因此我主要使用循环来重复内容。然而,这种方法在环境中以及在必须扩展内容的所有地方\foreach \x in \list都失败了。tabular

因此,我编写了一个\for可以在 中工作的宏tabular,但我想知道如何做到这一点更好的。 它不是完全地可扩展但可能也相当低效。

这没问题,因为大多数表格没有数千个单元格,因此性能通常不是问题。它适用于tabular在一行中生成多列的环境,或者(如果小心一点)生成多行的环境:

使用循环生成的一些表格


如上所述,这个循环显然存在一些问题,但大多数情况下它“对我有用”。我仍然想改进它,所以这些是最困扰我的几点:

  1. 每次迭代的循环体都使用 完全展开\edef。这可确保循环计数器在该迭代期间展开为其值。但是,这也要求用户使用 保护某些内容\noexpand,例如下表中的\pgfmathparse\\和。此外,它还需要对嵌套循环\hline进行双重保护(可能是三重?)。\noexpand
  2. 显然它不是完全可扩展的,因为它使用了\def\edef,因此不可能在循环内部使用循环,比如说,另一个循环\edef来定义具有常规结构的宏。当然,完全可以\def扩展为循环调用的宏。

那么,如何解决这些问题呢?

  • 对于问题 1,如果能有一种扩展方法就好了仅有的在循环体中使用循环计数器的任何地方,但保留其他所有内容。这将消除所有\noexpands。
  • 对于问题 2,我怀疑可能会牺牲一些功能,例如灵活的循环计数器和条件(例如,\repeat{\i}{5}{some code using \i}而不是类似 C 的语法)。

但是我真的不知道如何解决这些问题。有人能给我一些正确的方向吗?是否有可能同时实现选择性扩展和完全可扩展性?

最后,代码如下:

\documentclass{article}

\begin{filecontents*}{myforloop.sty}
\usepackage{pgf}
\usepackage{pgffor}
\usepackage{ifthen}

% Example:
% \for (\i = 0; \i < 10; \i = \i + 1) {
%    some code using \i
% }

\def\for(#1=#2;#3;#4=#5)#6{%
    \def\forloopresult{}%                        Macro to store the result
    \pgfmathtruncatemacro{#1}{#2}%               Set initial value of counter
    \def\forloopinner{%                          Recursive loop macro
        \pgfmathparse{int(#3)}%                  Calculate loop condition
        \ifthenelse{1=\pgfmathresult\relax}{%    if loop condition is true:
            \toks@=\expandafter{\forloopresult}%   Get tokens from result macro
            \edef\forloopresult{\the\toks@#6}%     Append expanded code to result
            \pgfmathtruncatemacro{#1}{#5}%         Set new counter value
            \forloopinner%                         Recurse
        }{%                                      else:
            \forloopresult%                        Expand to result
        }%
    }%
    \forloopinner%                               Start looping
}
\end{filecontents*}
\usepackage{myforloop}

\newcommand\N{5}% Number of loop iterations

\begin{document}
\begin{tabular}{|r|*{\N}{r|}}
    \hline
    $x$:   \for(\x=0; \x<\N; \x=\x+1) {&\x}\\\hline
    $e^x$: \for(\x=0; \x<\N; \x=\x+1) {%
        & \noexpand\pgfmathparse{exp(\x)}\noexpand\pgfmathresult%
    }\\\hline
\end{tabular}

\vspace{1em}

\begin{tabular}{|l|l|}
\hline
$x$ & $e^x$ \\\hline
\for(\x=0; \x<\N; \x=\x+1) {%
    \x & \noexpand\pgfmathparse{exp(\x)}\noexpand\pgfmathresult
    \noexpand\\\noexpand\hline
}
\end{tabular}
\end{document}

答案1

有许多方法可以做到这一点,而且不会很复杂:我将使用 介绍几种方法expl3。首先,如果你不介意让事情不可扩展,那么你可以这样做

\documentclass{article}
\usepackage{expl3}
\ExplSyntaxOn
\cs_new_protected:Npn \For #1#2#3#4
  {
    \int_step_inline:nnnn {#1} {#2} {#3 - 1} {#4}
  }
\cs_new_eq:NN \fpeval \fp_eval:n
\ExplSyntaxOff
\newcommand*\N{5}
\begin{document}

\begin{tabular}{|r|*{\N}{r|}}
    \hline
    $x$:   \For{0}{1}{\N}{&#1}\\
    \hline
    $e^x$: \For{0}{1}{\N}{& \fpeval{round(exp(#1),5)}}\\
    \hline
\end{tabular}
\end{document}

然后可以在使用点提供实现单元的代码。您可以改为使用两部分设置,其中函数是预编码的:然后在使用点进行扩展

\documentclass{article}
\usepackage{expl3}
\ExplSyntaxOn
\cs_new_protected:Npn \For #1#2#3#4
  {
    \int_step_function:nnnN {#1} {#2} {#3 - 1} #4
  }
\cs_new_eq:NN \fpeval \fp_eval:n
\ExplSyntaxOff
\newcommand*\N{5}
\newcommand\CellNumber[1]{&#1}
\newcommand\CellExp[1]{&\fpeval{round(exp(#1),5)}}
\begin{document}
\begin{tabular}{|r|*{\N}{r|}}
    \hline
    $x$:   \For{0}{1}{\N}\CellNumber\\
    \hline
    $e^x$: \For{0}{1}{\N}\CellExp\\
    \hline
\end{tabular}
\end{document}

在这两种情况下,\int_step_...函数的工作原理都类似大卫的回答:循环本身是通过扩展完成的。您不能对\x或进行类似赋值并具有可扩展性。

如果您使用 LuaTeX,另一种方法是在 Lua 中完成所有操作:这里的原语是可扩展的,因此您可以使用可能更熟悉的方法(问题表明您有 C 背景)。

\documentclass{article}
\newcommand\For[4]{%
  \directlua{%
    for x = #1, (#3 - 1), #2 do
      tex.print(#4)
    end
  }%
}
\newcommand*\N{5}
\makeatletter
\let\Percent\@percentchar
\makeatother
\begin{document}

\begin{tabular}{|r|*{\N}{r|}}
    \hline
    $x$:   \For{0}{1}{\N}{"&" .. x}\\
    \hline
    $e^x$: \For{0}{1}{\N}{"&" .. string.format("\Percent.3f", math.exp(x))}\\
    \hline
\end{tabular}
\end{document}

对于数字格式,我们需要%在 TeX 中有点尴尬:我为 LaTeX 内核命令提供了一个“用户”名称\@percentchar%不是一个注释字符)。

答案2

此循环仅使用扩展。为了用作#1循环计数器,最简单的方法是每次都重新定义它,如果您不想访问计数器,您可以更轻松地使用\myrepeat{5}{some code}注意我没有使用,\repeat因为该名称在现有语法中已被使用\loop

\documentclass{article}

\def\myrepeat#1{\ifnum#1=0 \expandafter\stopmyrepeat\fi 
[This is iteration \number#1]
\expandafter\myrepeat\expandafter{\the\numexpr#1-1\relax}}

\def\stopmyrepeat#1\myrepeat#2#3{}

\begin{document}


\myrepeat{18}

\end{document}

答案3

感谢 David 和 Joseph 使用 来#1作为循环计数器的占位符,我得以改进我在问题中给出的实现。我将其发布在这里,主要是作为学习练习,供任何碰巧有类似用例的人参考。此版本仅有的扩展循环计数器并且保留所有其他标记不变,从而无需使用来\noexpand保护不喜欢扩展的东西\pgfmath*

我认为使用宏比使用 Lua 的优势在于更容易指定循环体。使用 Lua 时,必须与tex.printstring.format%字符之类的东西作斗争。相比之下,宏版本允许您使用 LaTeX 代码简单地编写循环体,就像您在没有循环的情况下手动编写循环体一样。

现在,循环的调用如下所示,其中名称\i仅在内部使用(我将其留在那里主要是因为我喜欢这种语法),并且在循环体内不可用。而#1必须使用它来访问计数器值:

\For(\i=0; \i<5; \i=\i+1) { Current value of i: #1 }

当然,它仍然无法扩展,但似乎运行得非常好,甚至可以使用嵌套循环,使用##1####1作为嵌套计数器。在快速测试中唯一不起作用的(见下文)是\multicolumn在循环中使用:它抱怨Misplaced \omit.,这似乎发生在命令前有“东西”的时候\multicolumn,尽管我无法弄清楚在这种情况下到底是什么问题。

这是编译的示例,后面是生成它的代码。

使用循环创建的一些表格

\documentclass{article}

% Some test data for \csvread
\begin{filecontents*}{test.csv}
1,2,3,4,5,6,7
4,5,6,7,8,9,0
\end{filecontents*}

% The actual loop package
\begin{filecontents*}{myforloop.sty}
\usepackage{pgf}
\usepackage{ifthen}

% Example (spaces optional):
% \For (\i = 0; \i < 10; \i = \i + 1) {
%   some code using #1
% }

\newtoks\For@R% Accumulator for tokens from all iterations
\newtoks\For@B% Loop body with #1 replaced by current counter value

\def\For(#1=#2;#3;#4=#5)#6{%
    \let\EA=\expandafter%                          For shorter writing ;-)
    \def\For@result{}%                             Macro to store the result
    \pgfmathtruncatemacro{#1}{#2}%                 Set initial value of counter
    \def\For@body##1{#6}%                          Loop body for replacing counter
    \def\For@inner{%                               Recursive loop macro
        \pgfmathparse{int(#3)}%                    Calculate loop condition
        \ifthenelse{1=\pgfmathresult\relax}{%      if loop condition is true:
            \For@R=\EA{\For@result}%                 Get current result tokens
            \For@B=\EA\EA\EA{\EA\For@body\EA{#1}}%   Replace *only* counter in body
            \edef\For@result{\the\For@R\the\For@B}%  Append current body to result
            \pgfmathtruncatemacro{#1}{#5}%           Set new counter value
            \For@inner%                              Recurse
        }{%                                        else:
            \For@result%                             Expand to result
        }%
    }%
    \For@inner%                                  Start looping
}
\end{filecontents*}

\usepackage{myforloop}
\usepackage{csvsimple}

\newcommand\N{4}% Number of loop iterations

\begin{document}
% Generate multiple columns in a single row
\begin{tabular}{|r|*{\N}{r|}}
    \hline
    $x$:   \For(\i=0; \i<\N; \i=\i+1) {&#1}\\\hline
    $e^x$: \For(\i=0; \i<\N; \i=\i+1) {& \pgfmathparse{exp(#1)}\pgfmathresult}\\\hline
\end{tabular}
\\[1em]

% Generate multiple rows with a single loop
\begin{tabular}{|l|l|}
    \hline
    $x$ & $e^x$ \\\hline
    \For(\i=0; \i<\N; \i=\i+1) {%
        #1 & \pgfmathparse{exp(#1)}\pgfmathresult \\\hline
    }
\end{tabular}
\\[1em]

% Generate a multi-dimensional table using nested loops
\begin{tabular}{l*{\N}{r}}
    \For(\k=0; \k<3; \k=\k+1) {%
        Group #1:\\
        \For(\i=0; \i<\N; \i=\i+1) {%
            Row ##1: \For(\j=0; \j<\N; \j=\j+1) {& #1,##1,####1} \\
        }
    }
\end{tabular}
\\[1em]

% Generate columns 2 to 7: \csvcoli to \csvcolvii:
\csvreader[tabular=*{8}{r}, no head]{test.csv}{}{
    \csvcoli \For(\i=2; \i<=7; \i=\i+1) {& \csname csvcol\romannumeral ##1\endcsname}
}
\\[1em]

% Some things do not work :-(
\begin{tabular}{lll}
    % Complains about `Misplaced \omit.`
    %\For(\k=0; \k<\N; \k=\k+1) {\multicolumn{3}{c}{center}}
\end{tabular}
\\[1em]

\end{document}

如果你读到这里,你可能会同意当前版本仍有改进的空间。例如,两个令牌寄存器和可能\edef可以用单个令牌寄存器分配替换。但是我找不到正确的\expandafters 序列来展开计数器宏#1,然后是循环体\For@body{counter-value},然后是电流累加器\the\For@R。我能想到的最好的是:

\For@R=\EA\EA\EA{\EA\the\EA\For@R\EA\For@body\EA{#1}}

但这触及了某种内部 TeX 限制。从我有限的经验来看,这可能还不够\EA。;-)

相关内容