1

I am trying to create a macro that takes a variable number of arguments and creates a table where each argument becomes one row; so assuming the command is called dynamictable, calling it like this

\dynamictable{One & Two}{Three & Four}...{N-1 & N}

should produce:

One   Two
Three Four
...
N-1   N

I've taken inspiration from this blog post and transitively answers to this question on tex.stackexchange. However, I've only been able to generate one row without errors with the MWE below - which is hardly very dynamic!

\documentclass{minimal}
\usepackage{supertabular}

\makeatletter
\newcommand{\dynamictable}[1]{%
    \begin{supertabular}[l]{c l}
    #1 \checknextline
    \end{supertabular}
}

\newcommand{\checknextline}{\@ifnextchar\bgroup{\gobblenextline}{\\}}
\newcommand{\gobblenextline}[1]{#1\@ifnextchar\bgroup{\gobblenextline}{}}
% This attempt gives error: Misplaced alignment character &. 
%\newcommand{\gobblenextline}[1]{#1\\\@ifnextchar\bgroup{\gobblenextline}{}}
\makeatother


\begin{document}

\dynamictable{(1,1) & (1,2)}\par
% Error: Misplaced alignment character &.
%\dynamictable{(1,1) & (1,2)}{(2,1) & (2,2)}\par

\end{document}

Question: How can the above example be fixed to generate more than one row? Any suggestions are greatly appreciated!

  • Related: tex.stackexchange.com/questions/367979/… – Andrew Jun 27 '19 at 5:36
  • 2
    Do you really believe that \dynamictable{a & b}{c & d} is better and easier to maintain than a & b \\ c & d? – egreg Jun 27 '19 at 8:55
  • @egreg Right, I thought of that too after making the simplification that does everything with \checknextline (no need for \gobblenextline). If we make this recursive \checknextline take a delimited argument as opposed to a brace-delimited one, we are back to a tabular-like syntax, and I'm not sure of the usefulness of all this—apart from trying to improve one's LaTeX error diagnosing and programming skills. :-) – frougon Jun 27 '19 at 9:45
  • (well, with a delimited argument, there could be problems in case of nesting, though—the kind of problems that brace-delimited arguments prevent) – frougon Jun 27 '19 at 9:53
  • @egreg, no i do not :-) It's for a custom template, where I'd like the table logic to be separated from the content – trolle3000 Jun 27 '19 at 14:10
4

As pointed out by egreg, what we gain from such a \dynamictable command accepting an “arbitrary number of arguments”, as compared to using standard tabular markup where rows are terminated by \\, is not very clear. But let's study it as an exercise. :-)

What happens with your code

The error you get is due to the fact that your \checknextline call is immediately followed by \end{supertabular}.As a consequence, after \dynamictable has gobbled and inserted the first argument (1,1) & (1,2), \checknextline finds that the next token is the \end from \end{supertabular}; as it is not \let-equal to a \bgroup, \checknextline inserts \\ and doesn't recurse. So, the supertabular environment finishes, then TeX finds the second “argument” in the input stream. In fact, for TeX, this is not an argument but a braced group. TeX finds the & inside this group, and since we are not inside any alignment anymore (tabular & friends), this character token of category code 4 (alignment tab) is invalid, which gives the error:

./orig.tex:22: Misplaced alignment tab character &.
l.22 \dynamictable{(1,1) & (1,2)}{(2,1) &

How to fix it

Let's leave supertabular out of the game for now, it just complicates things. The first attempt at a fix could be (ugly, but shows the reasoning):

\documentclass{article}

\makeatletter
\newcommand{\dynamictable}[1]{%
    \begin{tabular}[l]{c l}
    #1 \checknextline
}

\newcommand{\checknextline}{\@ifnextchar\bgroup{\gobblenextline}{\\}}
\newcommand{\gobblenextline}[1]{#1\@ifnextchar\bgroup{\gobblenextline}{}}
\makeatother

\begin{document}

\dynamictable{(1,1) & (1,2)}{(2,1) & (2,2)}
\end{tabular}

\end{document}

This fails with:

./a.tex:16: Extra alignment tab has been changed to \cr.
<recently read> \endtemplate 

l.16 \dynamictable{(1,1) & (1,2)}{(2,1) & (2,2)}

because \checknextline calls \gobblenextline which inserts its #1 (the second “argument”) without prepending any \\. Therefore TeX finds two alignment tab tokens (&) on the same line of the alignment, which at this point looks like:

(1,1) & (1,2)(2,1) & (2,2)

This doesn't match the preamble you specified (at least 3 columns would be required), hence the error. Inserting the appropriate \\ can be done with:

\newcommand{\checknextline}{\@ifnextchar\bgroup{\gobblenextline}{}}
\newcommand{\gobblenextline}[1]{\\ #1\@ifnextchar\bgroup{\gobblenextline}{}}

With a document body like this:

\begin{document}

\dynamictable{(1,1) & (1,2)}{(2,1) & (2,2)}
\end{tabular}

\end{document}

this works flawlessly.

But this is not beautiful syntax, so as the next step, one could make an environment that inserts the \end{tabular} itself at the end, instead of a command that has to be followed by \end{tabular}:

\documentclass{article}

\makeatletter
\newenvironment{dynamictable}[1]{%
    \begin{tabular}[l]{c l}
    #1\checknextline
}{%
    \end{tabular}%
}

\newcommand{\checknextline}{\@ifnextchar\bgroup{\gobblenextline}{}}
\newcommand{\gobblenextline}[1]{\\ #1\@ifnextchar\bgroup{\gobblenextline}{}}
\makeatother

\begin{document}

\begin{dynamictable}{(1,1) & (1,2)}{(2,1) & (2,2)}
\end{dynamictable}

\end{document}

This works, but:

  • error reporting can be improved by using \tabular and \endtabular inside our custom environment, instead of \begin{tabular} and \end{tabular} (this way, if there is an error while processing the dynamictable environment, it will be reported as occurring inside this environment as opposed to within tabular which the end-user didn't explicitly use);

  • taking the first braced group as an argument of the environment doesn't seem very useful; the tabular contents could as well belong to the environment body.

Implementing these little improvements and customary whitespace handling for environments, we obtain:

\documentclass{article}

\makeatletter
\newenvironment{dynamictable}{%
  \tabular[l]{c l}
  \checknextline
}{%
  \unskip
  \endtabular
  \ignorespacesafterend
}

\newcommand{\checknextline}{\@ifnextchar\bgroup{\gobblenextline}{}}
\newcommand{\gobblenextline}[1]{#1\@ifnextchar\bgroup{\\ \gobblenextline}{}}
\makeatother

\begin{document}

\begin{dynamictable}
{(1,1) & (1,2)}
{(2,1) & (2,2)}
\end{dynamictable}

\end{document}

Screenshot

\@ifnextchar has the property of ignoring space tokens when looking ahead for the “next char”, which allowed us to put the two braced groups on separate lines in order to improve readability.

Alas, this doesn't work with supertabular (I haven't checked why), but it seems this package has been superseded by longtable for many use cases, and the approach described above does work with longtable:

\documentclass{article}
\usepackage{longtable}

\makeatletter
\newenvironment{dynamictable}{%
  \longtable[l]{c l}
  \checknextline
}{%
  \unskip
  \endlongtable
  \ignorespacesafterend
}

\newcommand{\checknextline}{\@ifnextchar\bgroup{\gobblenextline}{}}
\newcommand{\gobblenextline}[1]{#1\@ifnextchar\bgroup{\\ \gobblenextline}{}}
\makeatother

\begin{document}

\begin{dynamictable}
{(1,1) & (1,2)}
{(2,1) & (2,2)}
\end{dynamictable}

\end{document}

(screenshot identical to the previous one)

Retaining the command syntax

As pointed out by Skillmon, it is possible to retain the syntax of a command for \dynamictable as opposed to making it an environment (I believe I prefer the environment syntax, but you are of course free to choose whichever you prefer). For this, all that needs to be done is to insert the appropriate \endtabular or \endlongtable from within \checknextline and \gobblenextline when they detect that there is no other braced group following:

\documentclass{article}
\usepackage{longtable}

\makeatletter
\newcommand{\dynamictable}{%
  \longtable[l]{c l}
  \checknextline
}

\newcommand{\checknextline}{%
  \@ifnextchar\bgroup{\gobblenextline}{\endlongtable}%
}
\newcommand{\gobblenextline}[1]{%
  #1\@ifnextchar\bgroup{\\ \gobblenextline}{\endlongtable}%
}
\makeatother

\begin{document}

\dynamictable
{(1,1) & (1,2)}
{(2,1) & (2,2)}

\end{document}

Of course, you may write the body as:

\dynamictable{(1,1) & (1,2)}{(2,1) & (2,2)}

if you want. In case you use this approach, you can use \relax to terminate the table and be sure not to mistakenly take a following opening brace as the start of another line:

\dynamictable{(1,1) & (1,2)}{(2,1) & (2,2)}\relax
{\bfseries This doesn't belong to the table.}

Screenshot

That said, at this point, you should realize that we are back to using an end delimiter, only with less readability than with \end{dynamictable}. Thus, I still prefer the environment-based approach.

| improve this answer | |
  • 1
    Why don't you simply put \end{tabular} inside the second argument of \@ifnextchar? – Skillmon likes topanswers.xyz Jun 27 '19 at 6:53
  • @Skillmon Because I didn't think about it. :-) I believe I prefer the environment syntax for readability of the source, but your suggestion does work; I have therefore mentioned it at the end of my answer (cf. Retaining the command syntax). Thanks! – frougon Jun 27 '19 at 7:06
  • @Skillmon The downside with this approach is that an opening brace from the text that follows the table would be mistakenly interpreted as the start of a new table line. I've added the suggestion of using \relax at the end of the “arguments in variable number” to make sure this doesn't happen, but this leads us back to using an end delimiter, only with less readability than with \end{dynamictable}. So, I still prefer the environment approach—which doesn't change the fact that your remark was quite relevant to the question. – frougon Jun 27 '19 at 8:48
  • That is correct, but this wouldn't be an issue most of the time, and the few cases it could, the OP should be aware of how to circumvent this. – Skillmon likes topanswers.xyz Jun 27 '19 at 8:56
  • @Skillmon Right, I mostly agree. If \@ifnextchar were to expand tokens while trying to ignore space tokens before the potential “next char”, that would be more problematic, but that is not the case. However, the OP could still have bad surprises if he used a macro finishing with \dynamictable{...} ... {...} and the macro call were followed by an opening brace. Then he ought to add \relax (for instance) at the end of the replacement text of his macro to be sure to terminate the pseudo-argument grabbing regardless of what follows the macro call. – frougon Jun 27 '19 at 9:07
3

The following grabs arguments until the next token is not an opening brace. Additionally it takes a variable number of columns and sets them all centred. You can use an optional argument to explicitly specify the table preamble. This one uses tabular but I think you can easily change it to another table environment:

\documentclass[]{article}

\usepackage{array}

\makeatletter
\newcolumntype{\RepeatCols}{!{\d@llarend&\span\@gobbletwo}}
\newcommand*\dynamictable@grab
  {%
    \futurelet\dynamictable@tok\dynamictable@grab@a
  }
\newcommand*\dynamictable@grab@a
  {%
    \ifx\dynamictable@tok\@sptoken
      \dynamictable@grab@eatspace
    \fi
    \dynamictable@grab@b
  }
\newcommand*\dynamictable@grab@eatspace[1]
  {%
    \def\dynamictable@grab@eatspace\fi##1#1{\fi\dynamictable@grab}%
  }
\dynamictable@grab@eatspace{ }
\newcommand*\dynamictable@grab@b
  {%
    \ifx\dynamictable@tok\bgroup
      \dynamictable@grab@c
    \fi
    \end{tabular}%
  }
\newif\ifdynamictable@first
\def\dynamictable@grab@c\fi\end#1#2%
  {%
    \fi
    \ifdynamictable@first\else\\\fi #2
    \dynamictable@grab
  }
\newcommand\dynamictable[1][c \RepeatCols c]
  {%
    \begin{tabular}{#1}%
    \dynamictable@firsttrue
    \dynamictable@grab
  }
\makeatother

\begin{document}
\dynamictable{(1,1) & (1,2)}\par
% Error: Misplaced alignment character &.
\dynamictable{(1,1) & (1,2)}{(2,1) & (2,2)}\par
\dynamictable{(1,1) & (1,2) & (1,3)}{(2,1) & (2,2) & (2,3)}\par

\end{document}

The following is an alternative implementation, which takes an arbitrary number of arguments, but enclosed by an additional level of braces.

\documentclass[]{article}

\usepackage{array}

\makeatletter
\newcolumntype{\RepeatCols}{!{\d@llarend&\span\@gobbletwo}}
\begingroup
\xdef\dynamictable@mark
  {%
    \unexpanded\expandafter
      {%
        \csname dynamictable Error: mark expanded.\endcsname
        \dynamictable@mark
      }%
  }
\endgroup
\long\def\dynamictable@end\fi\@firstofone#1%
  {%
    \fi
    \end{tabular}%
  }
\newcommand\dynamictable@iterator[1]
  {%
    \ifx\dynamictable@mark#1%
      \dynamictable@end
    \fi
    \@firstofone{#1\\\dynamictable@iterator}%
  }
\newcommand\dynamictable[2][c \RepeatCols c]
  {%
    \begin{tabular}{#1}%
    \dynamictable@iterator#2\dynamictable@mark
  }
\makeatother

\begin{document}
\dynamictable{{(1,1) & (1,2)}}\par
% Error: Misplaced alignment character &.
\dynamictable{{(1,1) & (1,2)}{(2,1) & (2,2)}}\par
\dynamictable{{(1,1) & (1,2) & (1,3)}{(2,1) & (2,2) & (2,3)}}\par

\end{document}

enter image description here

(output of both code versions)

| improve this answer | |
  • Highly elegant, thanks! – trolle3000 Jun 27 '19 at 14:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.