5

How to pair items from two comma-separated lists (assuming they have the same length) in another list?

I did my best looking for the answer before asking… so I do hope this is not a duplicate!

For example, given to lists \def\a{1,2,3,4,5} and \def\b{a,b,c,d,e} I want to define \parlists such that \def\c{\pairlists[=]{\a}{\b}} will define \c as {1=a,2=b,3=c,4=d,5=e}.

I'm no TeX expert at all but, still, I had partial success: it works when the lists are given as parameters, but not when the lists are stored in the commands \a and \b. See my attempt below… Any help will be much appreciated. Thanks.

\documentclass{minimal}

\usepackage{xifthen}

\makeatletter
\def\@pairitems[#1]#2,#3\@nil#4,#5\@nil{%
% [#1][#2,#3][#4,#5]%
  \ifthenelse{\equal{#3}{}}{%
    \ifthenelse{\equal{#5}{}}%
      {#2#1#4}%
      {\PackageError{example}{Lists are not the same size}}%
  }{%
    \ifthenelse{\equal{#5}{}}%
      {\PackageError{example}{Lists are not the same size}}%
      {#2#1#4, \@pairitems[#1]#3\@nil#5\@nil}%
  }%
}
\def\pairitems[#1]#2#3{\@pairitems[#1]#2,\@nil#3,\@nil}
\makeatother


\begin{document}

\def\a{1,2,3,4,5}
\def\b{a,b,c,d,e}
\def\x{\pairitems[=]{1,2,3,4,5}{a,b,c,d,e}}
\def\y{\pairitems[=]{\a}{\b}}

\noindent
x: \x\\
y: \y\\

\end{document}

enter image description here

4
  • 2
    If you use \edef\y{\noexpand\pairitems[=]{\a}{\b}} then you get the same output in the second line.
    – user227987
    Nov 7, 2020 at 1:44
  • 1
    Related: tex.stackexchange.com/questions/554334/…
    – Cicada
    Nov 7, 2020 at 2:17
  • 1
    minimal class is for testing package loading dependencies. It defines almost zero commands/settings. For MWEs, use standard classes like book, article, etc.
    – Cicada
    Nov 7, 2020 at 2:30
  • 1
    beware using \def all three of \a \b and \c are core latex commands defined in the format and this over-writes all three with no warning. Nov 7, 2020 at 9:44

5 Answers 5

2

This is a solution using LaTeX3 approach. It is worth noticing that LaTeX is a macro language. You can use the \meaning to show the definition of a command. When you define \def\x{\pairitems{\a}{\b}}, then \x is \pairitems{\a}{\b}, literally, instead the value of \pairitems{\a}{\b}. If you wish \x to contain the value of \pairitems{\a}{\b}, some special treatment needs to be done.

\documentclass{minimal}
\usepackage[T1]{fontenc}
\usepackage{expl3}
\usepackage{xparse}


\ExplSyntaxOn

\clist_new:N \l_doc_tmpa_clist
\clist_new:N \l_doc_tmpb_clist
\seq_new:N \l_doc_tmpa_seq

\msg_new:nnn {doc} {difflen} {two~comma~separated~lists~have~different~length}


\cs_set:Npn \doc_pair_items:nnn #1#2#3 {
    \clist_set:Nn \l_doc_tmpa_clist {#2}
    \clist_set:Nn \l_doc_tmpb_clist {#3}
    \seq_clear:N \l_doc_tmpa_seq
    
    \int_compare:nNnF {\clist_count:N \l_doc_tmpa_clist} = {\clist_count:N \l_doc_tmpb_clist} {
        \msg_error:nn {doc} {difflen}
    }
    
    \int_step_inline:nn {\clist_count:N \l_doc_tmpa_clist} {
        \seq_put_right:Nn \l_doc_tmpa_seq {
            \clist_item:Nn \l_doc_tmpa_clist {##1}
            #1
            \clist_item:Nn \l_doc_tmpa_clist {##1}
        }
    }
    
    \seq_use:Nn \l_doc_tmpa_seq {,}
}

\cs_generate_variant:Nn \doc_pair_items:nnn {nxx}
\cs_generate_variant:Nn \doc_pair_items:nnn {noo}

\newcommand{\pairitems}[3][=]{
    \doc_pair_items:nnn {#1} {#2} {#3}
}

\newcommand{\pairitemso}[3][=]{
    \doc_pair_items:noo {#1} {#2} {#3}
}

\newcommand{\pairitemsx}[3][=]{
    \doc_pair_items:nxx {#1} {#2} {#3}
}

\ExplSyntaxOff


\begin{document}

\par\pairitems{1,2,3,4,5}{a,b,c,d,e}
\par\pairitems[+]{1,2,3,4,5}{a,b,c,d,e}

\def\a{1,2,3,4,5}
\def\b{a,b,c,d,e}
\par\pairitems{\a}{\b}
\par\pairitemso{\a}{\b}

\def\x{\pairitemso{\a}{\b}}
\par\meaning\x
\edef\x{\noexpand\pairitemso{\a}{\b}}
\par\meaning\x

\end{document}
2

This produces terminal output

> \zc=macro:
->1=a, 2=b, 3=c, 4=d, 5=e.

and typeset

enter image description here

Note the use of \unexpanded so that terms in the list should be protected from expansion, even though edef is used to save the result. I changed the calling order making the command define the specified token \zc

\documentclass{article}

% don't break latex accent support by redefining \a \b or \c which are
% all core latex commands....

\def\za{1,2,3,4,5}
\def\zb{a,b,c,d,e}

\newcommand\pairlists[4][=]{%
 \edef#2{%
 \expandafter\expandafter\expandafter\xpairlists
 \expandafter#3\expandafter,\expandafter\relax#4,\relax#1\zstop
 }}

\def\xpairlists#1,#2\relax#3,#4\relax#5\zstop{%
   \unexpanded{#1#5#3}%
   \ifcat$\detokenize{#2}$%
     \expandafter\gobblezstop
   \fi
   , \xpairlists#2\relax#4\relax#5\zstop}

\def\gobblezstop#1\zstop{}

\pairlists[=]{\zc}{\za}{\zb}

\show\zc

\begin{document}
\zc
\end{document}
3
  • this does assume the items are single tokens, that could be fixed if it's an issue. Nov 7, 2020 at 16:31
  • @UlrichDiez you could have left it (it was of course correct, I'd upvoted it:-) Nov 7, 2020 at 20:19
  • David, you're the closest I know to a magician. Thank you! Nov 8, 2020 at 18:18
1

You need to do some \expandafter-and-argument-exchanging-trickery in order to have the macros holding the lists of comma separated items expanded before \pairitems gets carried out.

In case you don't like to use \edef (which would trigger expansion of the comma-list's comma-separated items themselves, too), you can (ab?)use \romannumeral which triggers expansion until having gathered a valid TeX-⟨number⟩-quantity and in case that TeX-⟨number⟩-quantity denotes a non-positive value silently just gobbles the tokens forming that TeX-⟨number⟩-quantity.

\documentclass[a4paper, landscape]{article}

%===================[adjust margins/layout for the example]====================
\csname @ifundefined\endcsname{pagewidth}{}{\pagewidth=\paperwidth}%
\csname @ifundefined\endcsname{pdfpagewidth}{}{\pdfpagewidth=\paperwidth}%
\csname @ifundefined\endcsname{pageheight}{}{\pageheight=\paperheight}%
\csname @ifundefined\endcsname{pdfpageheight}{}{\pdfpageheight=\paperheight}%
\textwidth=\paperwidth
\oddsidemargin=1.5cm
\marginparsep=.2\oddsidemargin
\marginparwidth=\oddsidemargin
\advance\marginparwidth-2\marginparsep
\advance\textwidth-2\oddsidemargin
\advance\oddsidemargin-1in
\evensidemargin=\oddsidemargin
\textheight=\paperheight
\topmargin=1.5cm
\footskip=.5\topmargin
{\normalfont\global\advance\footskip.5\ht\strutbox}%
\advance\textheight-2\topmargin
\advance\topmargin-1in
\headheight=0ex
\headsep=0ex
\pagestyle{plain}
\parindent=0ex
\parskip=0ex 
\topsep=0ex
\partopsep=0ex
%==================[eof margin-adjustments]====================================

\makeatletter
\newcommand\Exchange[2]{#2#1}%
\newcommand\CheckWhetherNull[1]{%
  \ifcat Y\detokenize{#1}Y%
  \expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
}%
\@ifdefinable\@pairitems{%
  \long\def\@pairitems#1#2,#3\@nil#4,#5\@nil#6{%
    \CheckWhetherNull{#3}{%
      \CheckWhetherNull{#5}%
        {\z@#6#2#1#4}%
        {\z@\PackageError{example}{Lists are not the same size}}%
    }{%
      \CheckWhetherNull{#5}%
        {\z@\PackageError{example}{Lists are not the same size}}%
        {\@pairitems{#1}#3\@nil#5\@nil{#6#2#1#4,}}%
    }%
  }%
}%
\newcommand\pairitems[3]{\romannumeral\@pairitems{#1}#2,\@nil#3,\@nil{}}
\makeatother


\begin{document}

\newcommand*\one{1}
\newcommand*\two{2}
\newcommand*\three{3}
\newcommand*\four{4}
\newcommand*\five{5}
\newcommand*\MYa{a}
\newcommand*\MYb{b}
\newcommand*\MYc{c}
\newcommand*\MYd{d}
\newcommand*\MYe{e}

\newcommand*\ListA{\one,\two,\three,\four,\five}
\newcommand*\ListB{\MYa,\MYb,\MYc,\MYd,\MYe}
\newcommand*\PairedU{\pairitems{=}{\one,\two,\three,\four,\five}{\MYa,\MYb,\MYc,\MYd,\MYe}}
\newcommand*\PairedV{\expandafter\Exchange\expandafter{\expandafter{\ListB}}{\expandafter\Exchange\expandafter{\expandafter{\ListA}}{\pairitems{=}}}}
\expandafter\newcommand\expandafter*\expandafter\PairedW\expandafter{%
  \romannumeral0\Exchange{ }{\expandafter\expandafter\expandafter}\pairitems{=}{\one,\two,\three,\four,\five}{\MYa,\MYb,\MYc,\MYd,\MYe}%
}
\expandafter\newcommand\expandafter*\expandafter\PairedX\expandafter{%
  \romannumeral0%
  \expandafter\Exchange\expandafter{\expandafter{\ListB}}{%
    \expandafter\Exchange\expandafter{\expandafter{\ListA}}{%
      \Exchange{ }{\expandafter\expandafter\expandafter}\pairitems{=}%
    }%
  }%
}

\csname @ifdefinable\endcsname\PairedY{%
  \edef\PairedY{\pairitems{=}{\one,\two,\three,\four,\five}{\MYa,\MYb,\MYc,\MYd,\MYe}}%
}%

\csname @ifdefinable\endcsname\PairedZ{%
  \edef\PairedZ{\expandafter\Exchange\expandafter{\expandafter{\ListB}}{\expandafter\Exchange\expandafter{\expandafter{\ListA}}{\pairitems{=}}}}%
}%

\noindent{\ttfamily \string\PairedU:\\\meaning\PairedU}\\$\to$\PairedU\bigskip

\noindent{\ttfamily \string\PairedV:\\\meaning\PairedV}\\$\to$\PairedV\bigskip

\noindent{\ttfamily \string\PairedW:\\\meaning\PairedW}\\$\to$\PairedW\bigskip

\noindent{\ttfamily \string\PairedX:\\\meaning\PairedX}\\$\to$\PairedX\bigskip

\noindent{\ttfamily \string\PairedY:\\\meaning\PairedY}\\$\to$\PairedY\bigskip

\noindent{\ttfamily \string\PairedZ:\\\meaning\PairedZ}\\$\to$\PairedZ

\end{document}

enter image description here

Of course all this does not take the case of two empty lists into account.
Also there is no treatment of space-tokens surrounding comma-separated list items.
Also there is no special treatment of blank/empty list items.

1

You can't use \def\x{\pairlists...} (and using \def in documents is not really recommended anyway).

Here's an implementation in expl3. The command \mergelists has four arguments:

  1. #1 (optional, default =) what to put in the middle of the items
  2. #2 is the name for the new list
  3. #3 is the first list to merge, either with a symbolic name or explicit
  4. #4 is the second list to merge, either with a symbolic name or explicit

If the lists don't match by number of items, an error is raised.

\documentclass{article}
%\usepackage{xparse} % not needed for LaTeX 2020-10-01 or later

\ExplSyntaxOn

\NewDocumentCommand{\mergelists}{O{=}mmm}
 {
  \joaolourenco_mergelists:Nnnn #2 { #3 } { #4 } { #1 }
 }

\seq_new:N \l__joaolourenco_first_seq
\seq_new:N \l__joaolourenco_second_seq
\clist_new:N \l__joaolourenco_temp_clist

\cs_new_protected:Nn \joaolourenco_mergelists:Nnnn
 {
  % #1 = name for the merged list
  % #2 = first list
  % #3 = second list
  % #4 = separator

  % check whether #2 is a symbolic name and fill in the sequence
  \bool_lazy_and:nnTF { \tl_if_single_p:n { #2 } } { \token_if_cs_p:N #2 }
   {
    \seq_set_from_clist:NN \l__joaolourenco_first_seq #2
   }
   {
    \seq_set_from_clist:Nn \l__joaolourenco_first_seq { #2 }
   }
  % check whether #3 is a symbolic name and fill in the sequence
  \bool_lazy_and:nnTF { \tl_if_single_p:n { #3 } } { \token_if_cs_p:N #3 }
   {
    \seq_set_from_clist:NN \l__joaolourenco_second_seq #3
   }
   {
    \seq_set_from_clist:Nn \l__joaolourenco_second_seq { #3 }
   }

  % check whether the lists have the same number of elements
  \int_compare:nTF
   {
    \seq_count:N \l__joaolourenco_first_seq = \seq_count:N \l__joaolourenco_second_seq 
   }
   {% true, do the real job
    \__joao_lourenco_mergelists:Nn #1 { #4 }
   }
   {% false
    \PackageError{example}{Lists~are~not~the~same~size}{Your~lists~have~different~size}
   }
 }

\cs_new_protected:Nn \__joao_lourenco_mergelists:Nn
 {
  \clist_clear:N \l__joaolourenco_temp_clist
  \int_step_inline:nn { \seq_count:N \l__joaolourenco_first_seq }
   {
    \clist_put_right:Nx \l__joaolourenco_temp_clist
     {
      \seq_item:Nn \l__joaolourenco_first_seq { ##1 }
      #2
      \seq_item:Nn \l__joaolourenco_second_seq { ##1 }
     }
   }
  \clist_clear_new:N #1
  \clist_set_eq:NN #1 \l__joaolourenco_temp_clist
 }

\ExplSyntaxOff

\begin{document}

\def\lista{1,2,3,4,5}
\def\listb{a,b,c,d,e}

\mergelists{\listx}{\lista}{\listb}

\texttt{\meaning\listx}

\mergelists[+]{\listy}{\lista}{a,b,c,d,e}

\texttt{\meaning\listy}

\mergelists{\listz}{1,2,3,4}{a,b,c,d}

\texttt{\meaning\listz}

\mergelists{\listerror}{1,2,3}{a,b}

\end{document}

The output on the console will be

! Package example Error: Lists are not the same size.

See the example package documentation for explanation.
Type  H <return>  for immediate help.
 ...

l.87 \mergelists{\listerror}{1,2,3}{a,b}

? h
Your lists have different size

The PDF file will have

enter image description here

With \bool_lazy_and:nnTF we check that the argument is a single token which is a control sequence and, in this case, we assume it is the symbolic name for a list. Otherwise, the entire argument is the list.

The lists are converted to sequences; the number of items in each is compared. If they don't match, an error is raised. Otherwise, a clist is built from the items, with the separator in between.

Finally, the chosen control is assigned the value of the constructed clist.

1

I made the syntax a little different, if that works for the OP. Here, one executes \makepairlist[=]{\a}{\b} to create the desired pair list in a macro \thepairlist. In the MWE, I show that the detokenized macro is already expanded to the desired list.

If desired, one could follow up with \edef\c{\thepairlist} or, even better, \let\c\thepairlist.

\documentclass{article}
\usepackage{listofitems}
\newcommand\makepairlist[3][:]{%
  \readlist\ListA{#2}%
  \readlist\ListB{#3}%
  \def\thepairlist{}%
  \foreachitem\z\in\ListA[]{%
    \ifnum\zcnt=1\relax\else\edef\thepairlist{\thepairlist,}\fi
    \edef\thepairlist{\thepairlist\z#1\ListB[\zcnt]}%
  }%
}
\begin{document}
\def\a{1,2,3,4,5}
\def\b{a,b,c,d,e}
\makepairlist[=]{\a}{\b}
\thepairlist

\detokenize\expandafter{\thepairlist}
\end{document}

enter image description here

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .