Map arbitrary operation on zipped iteration over two comma-separated lists

EDIT to provide full answer to all scenarios posed by OP. As long as there are always two lists to combine, the effort can be done with listofitems.

Optional sub-argument separators can be multi-token; however they cannot include / or |, though if this is an issue, let me know. Perhaps some accommodation can be had.

\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage{listofitems}
\def\zipaux{}
\newcommand\zip[1][]{%
  \def\tmpA{#1}%
  \zipB%
}
\newcommand\zipB[1][]{%
  \expandafter\zipC\expandafter{\tmpA}{#1}%
}
\newcommand\zipC[5]{%
  \ifx\relax#1\relax
    \setsepchar{,}%
    \readlist*\argA{#3}%
    \ifx\relax#2\relax
      \renewcommand\zipaux[2]{#5}%
      \setsepchar{,}
    \else
      \renewcommand\zipaux[3]{#5}%
      \setsepchar{,/#2}
    \fi
  \else
    \setsepchar{,/#1}%
    \readlist*\argA{#3}%
    \ifx\relax#2\relax
      \renewcommand\zipaux[3]{#5}%
      \setsepchar{,}
    \else
      \renewcommand\zipaux[4]{#5}%
      \setsepchar{,/#2}
    \fi
  \fi%
  \readlist*\argB{#4}%
  \foreachitem\z\in\argB{%
    \ifx\relax#1\relax
      \ifx\relax#2\relax
        \zipaux{\argA[\zcnt]}{\argB[\zcnt]}%
      \else
        \zipaux{\argA[\zcnt]}{\argB[\zcnt,1]}{\argB[\zcnt,2]}%
      \fi
    \else
      \ifx\relax#2\relax
        \zipaux{\argA[\zcnt,1]}{\argA[\zcnt,2]}{\argB[\zcnt]}%
      \else
        \zipaux{\argA[\zcnt,1]}{\argA[\zcnt,2]}
               {\argB[\zcnt,1]}{\argB[\zcnt,2]}%
      \fi
    \fi
  }%
}
\begin{document}
\zip{a,b,c}{1,2,3}{#1-#2 }

\zip[?]{a?A,b?B,c?C}{1,2,3}{(#1*#2)-#3 }

\zip[?][::]{a?A,b?B,c?C}{1::X,2::Y,3::Z}{(#1*#2)-#3/#4 }

\zip[][::]{a,b,c}{1::X,2::Y,3::Z}{(#1-#2)/#3 }
\end{document}

enter image description here


The simple case with two arguments

enter image description here

\documentclass{article}

\begin{document}

\def\zip#1#2#3{%
\def\z##1##2{#3}%
\xzip#1,\relax#2,\relax}
\def\xzip#1,#2\relax#3,#4\relax{%
\z{#1}{#3}%
\if\relax\detokenize{#2}\relax
\expandafter\zzgobble
\fi
\xzip#2\relax#4\relax
}

\def\zzgobble#1\relax#2\relax{}


\zip{a,b,c}{1,2,3}{#1-#2 }
\end{document}

Defining \zip in the first way is quite easy:

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\zip}{mm +m}
 {
  \seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #1 }
  \seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #2 }
  \cs_set:Nn \__iagolito_zip:nn { #3 }
  \seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq \__iagolito_zip:nn
 }

\seq_new:N \l__iagolito_zip_a_seq
\seq_new:N \l__iagolito_zip_b_seq

\ExplSyntaxOff

\begin{document}

\zip{a,b,c}{1,2,3}{#1-#2 }

\end{document}

You can check this prints

a-1 b-2 c-3

If the two lists have different number of elements, the loop ends when either list ends.


The more complex features can be accomplished as well. Beware that the two optional argument must both appear, if the complex processing is needed.

The idea is to populate another sequence where the two lists are merged and then an auxiliary macro can be applied.

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\zip}{oomm +m}
 {
  \IfNoValueTF { #1 }
   { \iagolito_zip_simple:nnn { #3 } { #4 } { #5 } }
   { \iagolito_zip_full:nnnnn { #1 } { #2 } { #3 } { #4 } { #5 } }
 }

\seq_new:N \l__iagolito_zip_a_seq
\seq_new:N \l__iagolito_zip_b_seq
\seq_new:N \l__iagolito_zip_c_seq

\cs_new_protected:Nn \iagolito_zip_simple:nnn
 {
  \seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #1 }
  \seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #2 }
  \cs_set:Nn \__iagolito_zip:nn { #3 }
  \seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq \__iagolito_zip:nn
 }

\cs_new_protected:Nn \iagolito_zip_full:nnnnn
 {
  \seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #3 }
  \seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #4 }
  \seq_clear:N \l__iagolito_zip_c_seq
  \cs_set:Npn \__iagolito_zip_process:w #1 \q_stop #2 \q_stop { #5 }
  \seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq \__iagolito_merge:nn
  \seq_map_inline:Nn \l__iagolito_zip_c_seq { ##1 }
 }

\cs_new_protected:Nn \__iagolito_merge:nn
 {
  \seq_put_right:Nn \l__iagolito_zip_c_seq { \__iagolito_zip_process:w #1 \q_stop #2 \q_stop }
 }

\ExplSyntaxOff

\begin{document}

\zip{a,b,c}{1,2,3}{#1-#2 }

\zip[#1/#2][#3::#4]
    {a/A,b/B,c/C}
    {1::I,2::II,3::III}
    {Grand #1 is #2 but grand #3 is #4.\par}

\end{document}

enter image description here

If you want to pass macros expanding to lists, do one step expansion.

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\zip}{oomm +m}
 {
  \IfNoValueTF { #1 }
   { \iagolito_zip_simple:oon { #3 } { #4 } { #5 } }
   { \iagolito_zip_full:nnoon { #1 } { #2 } { #3 } { #4 } { #5 } }
 }

\seq_new:N \l__iagolito_zip_a_seq
\seq_new:N \l__iagolito_zip_b_seq
\seq_new:N \l__iagolito_zip_c_seq

\cs_new_protected:Nn \iagolito_zip_simple:nnn
 {
  \seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #1 }
  \seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #2 }
  \cs_set:Nn \__iagolito_zip:nn { #3 }
  \seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq \__iagolito_zip:nn
 }
\cs_generate_variant:Nn \iagolito_zip_simple:nnn { oo }

\cs_new_protected:Nn \iagolito_zip_full:nnnnn
 {
  \seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #3 }
  \seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #4 }
  \seq_clear:N \l__iagolito_zip_c_seq
  \cs_set:Npn \__iagolito_zip_process:w #1 \q_stop #2 \q_stop { #5 }
  \seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq \__iagolito_merge:nn
  \seq_map_inline:Nn \l__iagolito_zip_c_seq { ##1 }
 }
\cs_generate_variant:Nn \iagolito_zip_full:nnnnn { nnoo }

\cs_new_protected:Nn \__iagolito_merge:nn
 {
  \seq_put_right:Nn \l__iagolito_zip_c_seq { \__iagolito_zip_process:w #1 \q_stop #2 \q_stop }
 }

\ExplSyntaxOff

\newcommand{\listA}{a,b,c}
\newcommand{\listB}{1,2,3}
\newcommand{\listC}{a/A,b/B,c/C}
\newcommand{\listD}{1::I,2::II,3::III}

\begin{document}

\zip{a,b,c}{1,2,3}{#1-#2 }
\zip{\listA}{1,2,3}{#1-#2 }
\zip{a,b,c}{\listB}{#1-#2 }
\zip{\listA}{\listB}{#1-#2 }

\zip[#1/#2][#3::#4]
    {a/A,b/B,c/C}
    {1::I,2::II,3::III}
    {Grand #1 is #2 but grand #3 is #4.\par}

\zip[#1/#2][#3::#4]
    {\listC}
    {1::I,2::II,3::III}
    {Grand #1 is #2 but grand #3 is #4.\par}

\zip[#1/#2][#3::#4]
    {a/A,b/B,c/C}
    {\listD}
    {Grand #1 is #2 but grand #3 is #4.\par}

\zip[#1/#2][#3::#4]
    {\listC}
    {\listD}
    {Grand #1 is #2 but grand #3 is #4.\par}

\end{document}

The output is the same as before, just repeated four times for each instance.