3

When trying to maintain large tables with \cmidrules, I find them tedious to use, so I thought I'd define my own shortcut. Instead of writing \cmidrule{1-3} \cmidrule{4-5} I would simply write \cmidrulez{3,2} (because the first rule is 3 columns wide and the second one 2.

In order to support an arbitrary amount of comma-seperated arguments, I tried to adapt this answer:

\newcounter{mainargs}
\pgfkeys{mainargs/.is family, mainargs,
step counter/.code=\stepcounter{mainargs},
add argument/.style={step counter, arg\themainargs/.initial={#1}},
}

\newcounter{optargs} \newif\ifoptargs \pgfkeys{optargs/.is family, optargs, opt args present/.is if=optargs, step counter/.code=\stepcounter{optargs}, add argument/.style={opt args present=true, step counter, arg\theoptargs/.initial={#1}}, }

\newcommand{\cmidrulez}[2][]{% \setcounter{mainargs}{0}% \pgfkeys{mainargs, add argument/.list={#2}}% \setcounter{optargs}{0}% \pgfkeys{optargs, add argument/.list={#1}}% % \newcounter{cmrstart}% \newcounter{cmrend}% \setcounter{cmrstart}{1}% \ifoptargs% \foreach \n in {1,...,\theoptargs}{% \setcounter{cmrstart}{\pgfkeysvalueof{/optargs/arg\n}}% }% \fi% \foreach \n in {1,...,\themainargs}{% \setcounter{cmrend}{\value{cmrstart}}% \addtocounter{cmrend}{\pgfkeysvalueof{/mainargs/arg\n}}% \addtocounter{cmrend}{-1}% \cmidrule{\arabic{cmrstart}-\arabic{cmrend}}% \setcounter{cmrstart}{\value{cmrend}}% \stepcounter{cmrstart}% }% % }

Essentially what I'm doing is using two counters to calculate the corresponding numbers I need to give to the \cmidrule command. When I print the numbers with \arabic{cmrstart} and \arabic{cmrend} instead of calling \cmidrule the correct values appear, so this part is working.

However, when I try to use this in a table (where a \cmidrule would work) it complains about a "misplaced \noalign":

\cmidrule ->\noalign 
                     {\ifnum 0=`}\fi \@ifnextchar [{\@cmidrule }{\@cmidrule ...
l.66         \cmidrulez{2,1}
                             \\
I expect to see \noalign only after the \cr of
an alignment. Proceed, and I'll ignore this case.

! Missing } inserted. <inserted text> } l.66 \cmidrulez{2,1} \

I first thought this might have something to do with how I hand over the values, so I tried \value{cmrstart} and thecmrstart, the other two ways of which I know to get a value from a counter, but that didn't work. In fact, if I just replace the arguments of \cmidrule with hardcoded integers (inside my command definition), the problem persists, so it can't (only) be that.

When I define a much simpler command that just does something like \cmidrule{1-3} it works without problems, but as soon as there are arguments it fails:

% this works:
\newcommand{\cmidrules}{%
    \cmidrule{1-3}%
}
% this doesn't:
\newcommand{\cmidrules}[1][]{%
    \cmidrule{1-3}%
}

What am I doing wrong?

L3viathan
  • 131
  • 1
    You're writing a macro to replace a hyphen with a comma?? – Bernard Jun 17 '20 at 15:02
  • It cannot work: \cmidrule should not be preceded by anything else than other \cmidrule commands. With your \cmidrulez command you're adding something that makes TeX start a row of the tabular. – egreg Jun 17 '20 at 15:12
  • @Bernard No, I want to replace several \cmidrule invokations with a single auto-incrementing call to my command. – L3viathan Jun 17 '20 at 16:04
  • 1
    the macro has to be expandable so you can not do assignments or allocate counters, but even for normal cases you should not do \newcounter{cmrstart}% inside the macro just allocate the counter once in the package or document preamble. You do not want to allocate a new counter every time the macro is used. They are a finite resource. – David Carlisle Jun 17 '20 at 18:09
  • @DavidCarlisle Good to know, thank you. It's the first time I've used anything like this. And thank you for the "expandable" explanation, that was what was missing from my understanding. – L3viathan Jun 17 '20 at 18:26

3 Answers3

3

You cannot do assignments before issuing \cmidrule, unless they're inside \noalign.

Here's my version, where I also add * to leave a column empty. I also added (lr) for \cmidrule, otherwise \cmidrule{1-3}\cmidrule{4-5} would be exactly the same as \cmidrule{1-5}.

\documentclass{article}
\usepackage{booktabs}
\usepackage{xparse}

\ExplSyntaxOn \NewExpandableDocumentCommand{\cmidrulez}{m} { \noalign { __leviathan_cmidrulez:n { #1 } } \tl_use:N \g__leviathan_cmidrulez_tl }

\tl_new:N \g__leviathan_cmidrulez_tl \int_new:N \l__leviathan_cmidrulez_int

\cs_new_protected:Nn __leviathan_cmidrulez:n { \tl_gclear:N \g__leviathan_cmidrulez_tl \int_zero:N \l__leviathan_cmidrulez_int \clist_map_inline:nn { #1 } { \str_if_eq:nnTF { ##1 } { * } {% no rule here \int_incr:N \l__leviathan_cmidrulez_int } { \tl_gput_right:Nx \g__leviathan_cmidrulez_tl { \exp_not:N \cmidrule(lr) {% start \int_eval:n { \l__leviathan_cmidrulez_int + 1 } - \int_eval:n { \l__leviathan_cmidrulez_int + ##1 } } } \int_add:Nn \l__leviathan_cmidrulez_int { ##1 } } } }

\ExplSyntaxOff

\begin{document}

\begin{tabular}{{9}{c}} \toprule 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \ \cmidrulez{3,2} 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \ \cmidrulez{,3,*,2} 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \ \bottomrule \end{tabular}

\end{document}

The comma separated list is scanned and if the item is * the internal counter is just stepped. Otherwise the proper argument for \cmidrule is prepared by using the current value of the counter (plus 1) and the required number of steps. Then the counter is updated. The command is appended to a token list variable that's issued after \noalign has ended.

I'm not sure how much this can be useful, though. How many \cmidrule commands do you have in your document?

enter image description here

egreg
  • 1,121,712
3

A solution with an 'expandable' programmation.

\documentclass{article}
\usepackage{array}
\usepackage{booktabs}
\usepackage{xparse}

\ExplSyntaxOn \NewExpandableDocumentCommand { \clinez } { m } { __leviathan:nn { 1 } { #1 , } } \cs_new:Nn __leviathan:nn { \tl_if_empty:nF { #2 } { __leviathan:nnn #2 \q_stop { #1 } } } \cs_new:Npn __leviathan:nnn #1 , #2 \q_stop #3 { \str_if_eq:nnTF { #1 } { * } { __leviathan:nn { \int_eval:n { #3 + 1 } } { #2 } } { __leviathan:nn { \int_eval:n { #1 + #3 } } { #2 } \cmidrule (lr) { #3 - \int_eval:n { #3 + #1 - 1 } } } } \ExplSyntaxOff

\begin{document}

\begin{tabular}{{10}{c}} a & b & c & d & e & f & g & h & i & j \ \clinez{3,2,2} a & b & c & d & e & f & g & h & i & j \ \clinez{3,,2,2} a & b & c & d & e & f & g & h & i & j \ \end{tabular}

\end{document}

Output of the above code

F. Pantigny
  • 40,250
3

Macros in that position need to be expandable to avoid starting the next table cell.

It may be easier to see the expansion working directly rather than using package code in this case.

enter image description here

\documentclass{article}

\usepackage{array,booktabs}

\def\cmidrulez#1{\cmz{}1#1,{-1},} \def\gobblethree#1#2#3{} \def\cmz#1#2#3,{% \ifnum#3=-1 #1\expandafter\gobblethree\fi \cmz{#1\cmidrule(lr){#2-\the\numexpr#2+#3-1\relax}}{\the\numexpr#2+#3\relax}} \begin{document}

\begin{tabular}{ccccc} 1&2&3&4&5\ \cmidrulez{3,2} 1&2&3&4&5\ 1&2&3&4&5\ \end{tabular} \end{document}

David Carlisle
  • 757,742