5

I want to automatically generate a table from a comma separated list of key-value pairs such as {{x=1,y=2},{x=3,y=1},{x=2,y=3}}. In each "row" the "keys" are always the same (they become the column headers in the table), but the values can change. Given this particular list of key-value pairs the code below produces the following:

enter image description here

which is exactly what I want. Unfortunately marginally more complicated input for the values, such as {{x=1,y=$\overrightarrow{AB}$},{x=3,y=1},{x=2,y=3}}, breaks my code. My code does generate the expected output:

enter image description here

but before it does this I get compilation errors such as:

! Illegal parameter number in definition of \tableRows. \crcr l.40 ...\overrightarrow{AB}$},{x=3,y=1},{x=2,y=3}}

Is there s a better/more robust way of doing this?

Here's my code:

\documentclass{article}
\usepackage{xparse,etoolbox}

\newcounter{tableRow}       % for counting the number of rows in the table
\newcounter{tableCol}       % for counting the number of columns
\newcommand\tableRows{}     % to hold the contents of the table
\newcommand\tableHeader{}   % to hold the header of the table
\newcommand\MakeTable[1]{
  \setcounter{tableRow}{0}  % initialise
  \setcounter{tableCol}{1}
  \renewcommand\tableRows{}
  \renewcommand\tableHeader{}
  \forcsvlist\ProcessRow{#1}% generate table
  \begin{tabular}{*{\arabic{tableCol}}{c}}
    \tableHeader\\\hline\tableRows\\
  \end{tabular}
}
\makeatletter
\newcommand\ProcessRow[1]{% generate table rows using \ProcessEntry
  \addtocounter{tableRow}{1}
  \protected@xappto\tableRows{\Alph{tableRow}}% row label in table
  \forcsvlist\ProcessEntry{#1}
  \protected@xappto\tableRows{\\}
}
% need to extract key-value pairs from input of the form: key=val
\newcommand\ExtractKeyValuePair[2]{\def\Key{#1}\def\Value{#2}}
\DeclareDocumentCommand\ProcessEntry{>{\SplitArgument{1}{=}}m}{% add entries to row
    \ExtractKeyValuePair#1% key-value pairs --> \Key and \Value
    \ifnum\value{tableRow}=1%
       \addtocounter{tableCol}{1}
       \protected@xappto\tableHeader{&\Key}
    \fi
    \protected@xappto\tableRows{&\Value}
}

\begin{document}

  \MakeTable{{x=1,y=2},{x=3,y=1},{x=2,y=3}}

  % \MakeTable fails on this example
  \MakeTable{{x=1,y=$\overrightarrow{AB}$},{x=3,y=1},{x=2,y=3}}
\end{document}

To generate the rows of the table I use \protected@xappto from the etoolbox package. Initially I tried using tokens but I ran into expansion issues, due to my ignorance no doubt. It's not clear to me why I need \protected@xappto, rather than\xappto, but if I use \xappto the code fails with both examples.

The way that I extract the key-value pairs also feels a little OTT: I am using some trickery with \SplitArgument and \DeclareDocumentCommand from the xparse package to do this.

EDIT

It turns out that my code mostly works and that I was a little unlucky in that \overrightarrow was one of the first examples that I used in the real code that my MWE is distilled from. The problem with my MWE is that \overrightarrow is a fragile command. I can fix the compilation error in the MWE by adding the lines:

\usepackage{fixltx2e}
\MakeRobust{\overrightarrow}

This isn't a full fix, however, because there are bound to be other fragile commands that will break my code...Christian's approach of generating the table, rather than storing it, is probably the best solution.

  • Is the x=...,y=... way necessary at all? Why not using {1,2},{3,$\overrightarrow{AB}$} directly, splitting this? – user31729 Mar 24 '16 at 6:25
  • @ChristianHupfer Yes, in my application the key-values are necessary. I am repackaging something that is given to me this way. In addition, the keys give the entries in the table header. – Andrew Mar 24 '16 at 8:53
2

A solution, that uses package kvsetkeys for parsing the comma and key value lists. The table specification, the header row and the body are first constructed in token registers. Then the table is composed.

Since all assignments are kept local in a group, \MakeTable can be nested.

Package booktabs provides \midrule for a nicer line below the table header row.

Package alphalph provides \AlphAlph for alphabetic numbering of more than 26 rows.

Full example:

\documentclass{article}
\usepackage{booktabs}
\usepackage{kvsetkeys}
\usepackage{alphalph}

\makeatletter

% Resources
\newcount\TableRow
\newtoks\TableSpecification
\newtoks\TableHeader
\newtoks\TableBody

% Helper marco \AddToToks{<token register>}{<appended contents>}
\newcommand{\AddToToks}[2]{#1\expandafter{\the#1#2}}

% Main macro \MakeTable{...}
\newcommand{\MakeTable}[1]{%
  \begingroup
    \MakeTableSpecificationAndHeader{#1}%
    \MakeTableBody{#1}%
    \edef\TableBegin{%
      \noexpand\begin{tabular}{\TableSpecification}%
    }%
    %\TableBegin
    \begin{tabular}{\the\TableSpecification}%
      \the\TableHeader
      \midrule
      \the\TableBody
    \end{tabular}%
  \endgroup
}

\newcommand{\MakeTableSpecificationAndHeader}[1]{%
  \TableSpecification{l}%
  \TableHeader{}%
  \AnalyzeFirstRow{#1}%
  \AddToToks\TableHeader{\tabularnewline}%
}
\newcommand{\AnalyzeFirstRow}[1]{%
  \comma@parse{#1}\FirstRowProcessor
}
\newcommand{\FirstRowProcessor}[1]{%
  \kv@parse{#1}\FirstRowCellsProcessor
  \comma@break
}
\newcommand{\FirstRowCellsProcessor}[2]{%
  \AddToToks\TableSpecification{c}%
  \AddToToks\TableHeader{&#1}%
}

\newcommand{\MakeTableBody}[1]{%
  \TableRow=0 %
  \TableBody{}%
  \comma@parse{#1}\TableRowProcessor
}
\newcommand*{\TableRowProcessor}[1]{%
  \advance\TableRow by 1 %
  \edef\TableNumber{\AlphAlph{\TableRow}}%
  \expandafter\AddToToks\expandafter\TableBody\expandafter{\TableNumber}%
  \kv@parse{#1}\TableRowCellsProcessor
  \AddToToks\TableBody{\tabularnewline}%
}
% Simplified implementation, which requires, that all keys of
% a row are given and have the correct order.
\newcommand{\TableRowCellsProcessor}[2]{%
  \AddToToks\TableBody{&#2}%
}
\makeatother

\begin{document}
  \MakeTable{{x=1,y=2},{x=3,y=1},{x=2,y=3}}

  \medskip
  \MakeTable{{x=1,y=$\overrightarrow{AB}$},{x=3,y=1},{x=2,y=3}}

  \medskip
  \MakeTable{
    {
      xy=\MakeTable{{x=1, y=1}, {x=2, y=2}},
      yz=foo,
    },
    {
      xy=bar,
      yz=\MakeTable{{y=4, z=5}, {y=6, z=7}},
    },
  }
\end{document}

Result

  • Thanks Heiko. By using keys you have solved a problem that I didn't realise that I had. – Andrew Mar 24 '16 at 23:26
2

Here's a version without key-values (first), the only thing that has to be done manually is the adapt the number of columns here.

\documentclass{article}
\usepackage{xparse,etoolbox}

\newcounter{tableRow}       % for counting the number of rows in the table
\newcounter{tableCol}       % for counting the number of columns
\newcommand\tableRows{}     % to hold the contents of the table
\newcommand\tableHeader{}   % to hold the header of the table




\ExplSyntaxOn
\clist_new:N \g_andrew_clist
\int_new:N \l_columncounter_int
\int_new:N \g_list_count


\NewDocumentCommand{\ProcessRow}{m}{%
  \clist_gset:Nn \g_andrew_clist {#1}%
  \int_gset:Nn \g_list_count {\clist_count:N \g_andrew_clist}
  \int_gset:Nn \l_columncounter_int {\c_one}
  \stepcounter{tableRow}%
  \Alph{tableRow}  &
  \prg_replicate:nn { \g_list_count }{%
    \int_compare:nNnTF { \l_columncounter_int } < { \g_list_count }{%
      \clist_item:Nn \g_andrew_clist {\l_columncounter_int} &
    }{
      \clist_item:Nn \g_andrew_clist {\int_use:N \l_columncounter_int }
    }
    \int_gincr:N \l_columncounter_int 
  }
}


\newcommand\MakeTable[2][3]{%
  \clist_set:Nn \l_tmpa_clist {#2}  
  \setcounter{tableRow}{0}  % initialise
  \setcounter{tableCol}{#1}
  \begin{tabular}{*{\arabic{tableCol}}{c}}
    & x & y \tabularnewline
    \hline
    \clist_map_inline:Nn \l_tmpa_clist {%
      \ProcessRow{##1} \tabularnewline
    }
  \end{tabular}

}


\begin{document}


\ExplSyntaxOff
\MakeTable{ {1,2}, {5,6}, {3,{$\overrightarrow{AB}$}}}

\MakeTable[4]{ {1,2,3}, {4,5,6}, {7,8,{$\overrightarrow{AB}$}}}

\end{document}

enter image description here

Update

\documentclass{article}
\usepackage{xparse,etoolbox}
\usepackage{l3regex}

\newcounter{tableRow}       % for counting the number of rows in the table
\newcounter{tableCol}       % for counting the number of columns

\ExplSyntaxOn
\clist_new:N \g_andrew_argument_clist
\clist_new:N \g_andrew_clist
\clist_new:N \l_andrew_data_clist
\seq_new:N \l_andrew_header_seq

\int_new:N \l_columncounter_int
\int_new:N \g_list_count

\NewDocumentCommand{\ProcessRow}{m}{%
  \clist_gset:Nn \g_andrew_clist {#1}%
  \int_gset:Nn \g_list_count {\clist_count:N \g_andrew_clist}
  \int_gset:Nn \l_columncounter_int {\c_one}
  \stepcounter{tableRow}%
  \Alph{tableRow}  &
  \prg_replicate:nn { \g_list_count }{%
    \int_compare:nNnTF { \l_columncounter_int } < { \g_list_count }{%
      \clist_item:Nn \g_andrew_clist {\l_columncounter_int} &
    }{
      \clist_item:Nn \g_andrew_clist {\int_use:N \l_columncounter_int }
    }
    \int_gincr:N \l_columncounter_int 
  }
}

\NewDocumentCommand{\ProcessHeader}{m}{%
  \clist_gset_eq:NN \g_andrew_clist #1%
  \int_gset:Nn \g_list_count {\clist_count:N \g_andrew_clist}
  \int_gset:Nn \l_columncounter_int {\c_one}
  & %
  \prg_replicate:nn { \g_list_count }{%
    \int_compare:nNnTF { \l_columncounter_int } < { \g_list_count }{%
      \clist_item:Nn \g_andrew_clist {\l_columncounter_int} &
    }{
      \clist_item:Nn \g_andrew_clist {\int_use:N \l_columncounter_int }
    }
    \int_gincr:N \l_columncounter_int 
  }
}



\newcommand\MakeTable[1]{%
  \clist_set:Nn \g_andrew_argument_clist {#1} % Store the full clist first
  \clist_set:Nx \l_tmpb_clist {\clist_item:Nn \g_andrew_argument_clist {1}} % Extract the first line to get the header descriptions
  \int_set:Nn \l_tmpa_int {\clist_count:N \l_tmpb_clist}
  \int_incr:N \l_tmpa_int
  \seq_clear:N \l_andrew_header_seq
  \tl_set:Nn \l_tmpa_tl {#1} % Grab the argument again 
  \regex_replace_all:nnN { \w+= }{ } \l_tmpa_tl % Replace the x= values with nothing 
  \clist_set:NV \l_andrew_data_clist  {\l_tmpa_tl} %making a new clist again
  % Get the headers
  \clist_map_inline:Nn \l_tmpb_clist { %
    \tl_set:Nn \l_tmpa_tl {##1}
    \seq_clear:N \l_tmpb_seq
    \seq_set_split:NnV \l_tmpb_seq {=} {\l_tmpa_tl}
    \seq_gput_right:Nx \l_andrew_header_seq {\seq_item:Nn \l_tmpb_seq {1}}
  }
  \clist_set_from_seq:NN \l_tmpb_clist \l_andrew_header_seq
  \setcounter{tableRow}{0}  % initialise
  \setcounter{tableCol}{\int_use:N \l_tmpa_int}
  \begin{tabular}{*{\int_eval:n{\l_tmpa_int+1}}{c}}
    \ProcessHeader{\l_tmpb_clist}  \tabularnewline % Header frist
    \hline
    \clist_map_inline:Nn \l_andrew_data_clist {% 
      \ProcessRow{##1} \tabularnewline
    }
  \end{tabular}
}
\ExplSyntaxOff

\begin{document}
\MakeTable{ {x=1,Foo=2}, {5,6}, {3,{$\overrightarrow{AB}$}}}

\MakeTable{ {x=1,y=2,z=3,u=4}, {x=4,y=5,z=6,u=10}, {x=7,y=8,{z=$\overrightarrow{AB}$},u=14}}

\end{document}

enter image description here

  • Thanks. Manual adjustments are not an option for me as the number of keys, and their values, are not constrained in any way. I'm also quite curious as to why my code does not work. – Andrew Mar 24 '16 at 10:24
  • @Andrew: Ok, I will adjust – user31729 Mar 24 '16 at 14:43
  • @Andrew: See the update please. The number of columns is automatically detected as well as the column headers. I hope that those will appear always as x=... and not as Foo bar=... (i.e. no space in between!) – user31729 Mar 24 '16 at 16:17
2

You don't really want to use \protected@xappto, but rather \xappto and \expandonce.

(I also fixed the end-of-line protections.)

\documentclass{article}
\usepackage{xparse,etoolbox}

\newcounter{tableRow}       % for counting the number of rows in the table
\newcounter{tableCol}       % for counting the number of columns
\newcommand\tableRows{}     % to hold the contents of the table
\newcommand\tableHeader{}   % to hold the header of the table
\newcommand\MakeTable[1]{%
  \setcounter{tableRow}{0}% initialise
  \setcounter{tableCol}{1}%
  \renewcommand\tableRows{}%
  \renewcommand\tableHeader{}%
  \forcsvlist\ProcessRow{#1}% generate table
  \begin{tabular}{*{\arabic{tableCol}}{c}}
    \tableHeader\\\hline\tableRows\\
  \end{tabular}%
}
\newcommand\ProcessRow[1]{% generate table rows using \ProcessEntry
  \addtocounter{tableRow}{1}%
  \xappto\tableRows{\Alph{tableRow}}% row label in table
  \forcsvlist\ProcessEntry{#1}%
  \gappto\tableRows{\\}%
}
% need to extract key-value pairs from input of the form: key=val
\newcommand\ExtractKeyValuePair[2]{\def\Key{#1}\def\Value{#2}}
\DeclareDocumentCommand\ProcessEntry{>{\SplitArgument{1}{=}}m}{% add entries to row
    \ExtractKeyValuePair#1% key-value pairs --> \Key and \Value
    \ifnum\value{tableRow}=1
       \addtocounter{tableCol}{1}%
       \xappto\tableHeader{&\expandonce{\Key}}%
    \fi
    \xappto\tableRows{&\expandonce{\Value}}%
}

\begin{document}

\MakeTable{{x=1,y=2},{x=3,y=1},{x=2,y=3}}

\MakeTable{{x=1,y=$\overrightarrow{AB}$},{x=3,y=1},{x=2,y=3}}

\end{document}

enter image description here

An expl3 implementation, where I use (or abuse, perhaps) the key-value interface.

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\MakeTable}{m}
 {
  \andrew_maketable_main:n { #1 }
 }

\seq_new:N \l__andrew_maketable_cols_seq
\tl_new:N \l__andrew_maketable_body_tl
\int_new:N \l__andrew_maketable_rows_int

\keys_define:nn { andrew/maketable }
 {
  unknown .code:n = { \__andrew_maketable_add:n { #1 } }
 }

\cs_new_protected:Nn \andrew_maketable_main:n
 {
  \seq_clear:N \l__andrew_maketable_cols_seq
  \tl_clear:N \l__andrew_maketable_body_tl
  \int_zero:N \l__andrew_maketable_rows_int
  \clist_map_inline:nn { #1 }
   {
    \__andrew_maketable_add_row:n { ##1 }
   }
  \tl_put_left:Nx \l__andrew_maketable_body_tl
   { & \seq_use:Nn \l__andrew_maketable_cols_seq { & } \exp_not:n { \\ \hline } }
  \begin{tabular}{ c *{ \seq_count:N \l__andrew_maketable_cols_seq } { c } }
  \tl_use:N \l__andrew_maketable_body_tl
  \end{tabular}
 }

\cs_new_protected:Nn \__andrew_maketable_add_row:n
 {
  \int_incr:N \l__andrew_maketable_rows_int
  \tl_put_right:Nx \l__andrew_maketable_body_tl
   {
    \int_to_Alph:n { \l__andrew_maketable_rows_int }
   }
  \keys_set:nn { andrew/maketable } { #1 }
  \tl_put_right:Nn \l__andrew_maketable_body_tl { \\ }
 }

\cs_new_protected:Nn \__andrew_maketable_add:n
 {
  \seq_if_in:NxF \l__andrew_maketable_cols_seq { \l_keys_key_tl }
   {
    \seq_put_right:Nx \l__andrew_maketable_cols_seq { \l_keys_key_tl }
   }
  \tl_put_right:Nn \l__andrew_maketable_body_tl { & #1 }
 }

\ExplSyntaxOff

\begin{document}

\MakeTable{{x=1,y=2},{x=3,y=1},{x=2,y=3}}

\MakeTable{{x=1,y=$\overrightarrow{AB}$},{x=3,y=1},{x=2,y=3}}

\MakeTable{ {x=1,y=2,z=3,u=4}, {x=4,y=5,z=6,u=10}, {x=7,y=8,z=$\overrightarrow{AB}$,u=14}}

\end{document}

The sequence keeps track of the column headers; in the token list variable I store the rows.

Of course, like in the original implementation, the order must be strict or items would be misplaced.

enter image description here

1

An approach using package xinttools.

as in other implementations, the order must be strict. A more complicated approach is needed if one wants arbitrary order for the keys.

\documentclass{article}
\usepackage{xinttools}

% helper utilities
\newcommand*\AndrewExtractKey {}
\def\AndrewExtractKey #1#2=#3,{#1#2}
\newcommand*\AndrewExtractValue {}
\def\AndrewExtractValue #1=#2#3,{#2#3}
\makeatletter
    \newcommand*\JohnDoeAlph [1]{\@Alph{#1\relax}}
\makeatother

% Main command
\newcommand*\MakeTable [1]{%
  \fdef\AndrewTableKeys{\xintCSVtoList{#1}}%
  %
  \fdef\AndrewTableKeysFirstRow{\xintNthElt{1}{\AndrewTableKeys}}%
  \fdef\AndrewTableKeysNbColumns
      {\xintNthElt{0}{\xintCSVtoList{\AndrewTableKeysFirstRow}}}% 
  %
  \gdef\AndrewTableKeysRowCount{1}%
  \begin{tabular}{c*{\AndrewTableKeysNbColumns}c}
    \xintFor ##1 in {\AndrewTableKeysFirstRow}\do
      {&\AndrewExtractKey ##1,}\\
    \hline
    \xintFor* ##1 in {\AndrewTableKeys}\do
    {%
      \JohnDoeAlph{\AndrewTableKeysRowCount}%
      \xdef\AndrewTableKeysRowCount{\the\numexpr\AndrewTableKeysRowCount+1}%
      \xintFor ##2 in {##1}\do{&\AndrewExtractValue ##2,}\\%
    }%
  \end{tabular}
}

\begin{document}

\MakeTable{{x=1,y=2},{x=3,y=1},{x=2,y=3}}

\MakeTable{{x=1,y=$\overrightarrow{AB}$},{x=3,y=1},{x=2,y=3}}

\MakeTable{ {x=1,y=2,z=3,u=4}, {x=4,y=5,z=6,u=10}, {x=7,y=8,z=$\overrightarrow{AB}$,u=14}}

\end{document}

Blockquote

  • Thanks jfbu. I am always impressed with the power of xinttools. I had tried to deal with the ='s like you have but couldn't get it to work. What does first defining \newcommand*\AndrewExtractKey {} and then \def\AndrewExtractKey #1#2=#3,{#1#2} do? – Andrew Mar 24 '16 at 23:09
  • @Andrew the first \newcommand* is to check à la LaTeX whether the macro name is already in use, so at least we are warned we are trying to overwrite something. Then we switch to \def in order to use a delimited parameter text. By the way, both \AndrewExtractKey and \AndrewExtractValue should have been defined \long in order for the value part to be allowed to have an empty line or a \par. The thing with #1#2 is to get rid of initial spaces, this is a bit superfluous here in a tabular. – user4686 Mar 25 '16 at 7:03

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.