4

I want to make an apply function that can apply any single-argument command to a comma-separated list of arguments, and print the results separated by commas. (ETA: I want to make the command from scratch, i.e., without using another package dependency, as a way of understanding how all the subroutines work.)

I have a partially working solution, but it doesn't work when the command to be applied is complicated. See the following example, where \lowercase can be effectively applied across a list, but \ComplicatedCommand and \MoreComplicatedCommand cannot:

\RequirePackage{luatex85}
\documentclass[letterpaper]{article}


% This command is for applying a single command to a comma-separated list of tokens, and listing the results separated by ", "
\makeatletter
\newcommand{\apply}[3][, ]{
% #1: optional separator to print between applications; default=[, ]
% #2: command to apply; 
% #3: list to apply command to 
  \def\itemsep{\def\itemsep{#1}} % first call to \itemsep prints nothing; later calls print #1
  \@for \listelement:=#3\do{\itemsep#2\expandafter{\listelement}}%
}
\makeatother

\begin{document}

\apply{\lowercase}{THESE,WORDS,PRINT,IN,LOWERCASE.}

\newcommand{\ComplicatedCommand}[1]{\lowercase{#1}}
\apply{\ComplicatedCommand}{WHY,DO,THESE,WORDS,NOT,PRINT,IN,LOWERCASE?}

\newcommand{\MoreComplicatedCommand}[1]{\lowercase{\uppercase{{\lowercase{#1}}}}}
\apply{\MoreComplicatedCommand}{ULTIMATELY,I,WANT,APPLY,TO,MAKE,THESE,LOWERCASE,AS,WELL.}


\end{document}

The result:

example result

Can anyone fix my \newcommand{\apply} so that it can work for any \ComplicatedCommand as argument #2? If so, please provide a full working example, all the way from \documentclass to \end{document}, so anyone who reads the answer in the future can copy and paste it to see if it still works for their version of (Lua)TeX.

4 Answers 4

4

You have

\itemsep#2\expandafter

which means that if #2 is a macro with an argument as it is in your second two cases (\lowercase isn't a macro) then the argument will be \expandafter. You want to expand \expandafter before expanding #2. If you know #2 is always a single token you could just put extra \expandafter before it, but safer is:

so...

\documentclass[letterpaper]{article}


% This command is for applying a single command to a comma-separated list of tokens, and listing the results separated by ", "
\makeatletter
\newcommand{\apply}[3][, ]{
% #1: optional separator to print between applications; default=[, ]
% #2: command to apply; 
% #3: list to apply command to 
  \def\itemsep{\def\itemsep{#1}} % first call to \itemsep prints nothing; later calls print #1
  \def\zz{\itemsep#2}%
  \@for \listelement:=#3\do{\expandafter\zz\expandafter{\listelement}}%
}
\makeatother

\begin{document}

\apply{\lowercase}{THESE,WORDS,PRINT,IN,LOWERCASE.}

\newcommand{\ComplicatedCommand}[1]{\lowercase{#1}}
\apply{\ComplicatedCommand}{WHY,DO,THESE,WORDS,NOT,PRINT,IN,LOWERCASE?}

\newcommand{\MoreComplicatedCommand}[1]{\lowercase{\uppercase{{\lowercase{#1}}}}}
\apply{\MoreComplicatedCommand}{ULTIMATELY,I,WANT,APPLY,TO,MAKE,THESE,LOWERCASE,AS,WELL.}


\end{document}
2
  • note I dropped luatex85 as the behaviour isn't luatex related and even with luatex, you should not need luatex85 normally, that was/is just a temporary compatibility layer to give packages time to update when the luatex primitives changed. Jan 11, 2018 at 7:56
  • Thanks! Your solution is package-free and passes my tests, and helped me understand what's going on here :) Unfortunately, though, it didn't work in the broader context I was hoping to use it in, so I've created a follow-up question for using the output of this apply command in a \directlua call; here it is if you're interested. Jan 11, 2018 at 15:52
3

etoolbox already possesses list processing capabilities, so why not just piggyback on that:

enter image description here

\documentclass{article}

\usepackage{etoolbox}

% This command is for applying a single command to a comma-separated list of tokens, and listing the results separated by ", "
\newcommand{\apply}[3][, ]{%
  % #1: optional separator to print between applications; default = [, ]
  % #2: command to apply; 
  % #3: list to apply command to 
  \def\listitemsep{\def\listitemsep{#1}}% https://tex.stackexchange.com/a/89187/5764
  \renewcommand{\do}{\listitemsep #2}% Each item should be processed this way
  \docsvlist{#3}% Process entire list of items
}

\begin{document}

\apply{\MakeLowercase}{THESE,WORDS,PRINT,IN,LOWERCASE.}

\newcommand{\ComplicatedCommand}[1]{\MakeLowercase{#1}}%
\apply{\ComplicatedCommand}{WHY,DO,THESE,WORDS,NOT,PRINT,IN,LOWERCASE?}

\newcommand{\MoreComplicatedCommand}[1]{\MakeLowercase{\MakeUppercase{{\MakeLowercase{#1}}}}}%
\apply{\MoreComplicatedCommand}{ULTIMATELY,I,WANT,APPLY,TO,MAKE,THESE,LOWERCASE,AS,WELL.}

\end{document}

Each element is processed with \do, while the entire list is processed sequentially using \docsvlist.

2
  • There is also \forcsvlist which is even closer to \apply and \DeclareListParser*.
    – moewe
    Jan 11, 2018 at 8:46
  • Thanks a ton for this! But, I should have said in the original question (and have now added) that I want to code the example "from scratch", without other package dependencies, so that (a) it will run faster, and (b) I can better understand what's going on. Jan 11, 2018 at 14:38
3

Cleaner code with expl3; note how leading and trailing spaces in the input are ignored. The idea is to parse the comma separated list of items and add each one to a sequence, but as an argument to the given command. Then we can output the list with the required separator.

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\apply}{O{,~}mm}
 {% #1 = output separator, #2 = command to apply, #3 = list
  \critch_apply:Nnn { #2 } { #1 } { #3 }
 }

\seq_new:N \l_critch_apply_output_seq

\cs_new_protected:Nn \critch_apply:Nnn
 {
  \seq_clear:N \l_critch_apply_output_seq
  \clist_map_inline:nn { #3 }
   {
    \seq_put_right:Nn \l_critch_apply_output_seq { #1{##1} }
   }
  \seq_use:Nn \l_critch_apply_output_seq { #2 }
 }

\ExplSyntaxOff

\newcommand{\ComplicatedCommand}[1]{\lowercase{#1}}
\newcommand{\MoreComplicatedCommand}[1]{\lowercase{\uppercase{{\lowercase{#1}}}}}

\begin{document}

\apply{\lowercase}{THESE,WORDS,PRINT,IN,LOWERCASE.}

\apply{\ComplicatedCommand}{WHY,DO,THESE,WORDS,NOT,PRINT,IN,LOWERCASE?}

\apply[~--- ]{\MoreComplicatedCommand}{ULTIMATELY , I , WANT,APPLY ,TO,
  MAKE,THESE,LOWERCASE,AS,WELL.}

\end{document}

enter image description here

An extended version where the second argument can be also a set of instructions to apply to each item, where the item is represented by #1; if the second argument consists of a single macro, then it is assumed to be what you want to apply to each item. The first example could be typeset in the same way as

\apply{\lowercase{#1}}{THESE,WORDS,PRINT,IN,LOWERCASE.}

Here's the code.

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\apply}{O{,~}+mm}
 {% #1 = output separator, #2 = command to apply, #3 = list
  \critch_apply:nnn { #1 } { #2 } { #3 }
 }

\seq_new:N \l_critch_apply_output_seq

\cs_new_protected:Nn \critch_apply:nnn
 {
  \seq_clear:N \l_critch_apply_output_seq
  \bool_lazy_and:nnTF { \tl_if_single_p:n { #2 } } { \token_if_cs_p:N #2 }
   {
    \cs_gset_eq:NN \__critch_apply_command:n #2
   }
   {
    \cs_gset:Nn \__critch_apply_command:n { #2 }
   }
  \clist_map_inline:nn { #3 }
   {
    \seq_put_right:Nn \l_critch_apply_output_seq { \__critch_apply_command:n {##1} }
   }
  \seq_use:Nn \l_critch_apply_output_seq { #1 }
 }

\ExplSyntaxOff

\newcommand{\ComplicatedCommand}[1]{\lowercase{#1}}
\newcommand{\MoreComplicatedCommand}[1]{\lowercase{\uppercase{{\lowercase{#1}}}}}

\newcommand{\surround}[3]{#1\textit{#3}#2}

\begin{document}

\apply{\lowercase}{THESE,WORDS,PRINT,IN,LOWERCASE.}

\apply{\ComplicatedCommand}{WHY,DO,THESE,WORDS,NOT,PRINT,IN,LOWERCASE?}

\apply[~--- ]{\MoreComplicatedCommand}{ULTIMATELY , I , WANT,APPLY ,TO,
  MAKE,THESE,LOWERCASE,AS,WELL.}

\apply[ $|$ ]{(#1)}{a, list,of , words}

\begin{tabular}[t]{@{}c@{}}
\apply[\\]{\surround{-}{!}{#1}}{a, list,of , words}
\end{tabular}

\end{document}

enter image description here

0

Tailor made for listofitems.

\documentclass{article}
\usepackage{listofitems}
\newcommand\apply[2]{%
  \readlist*\arglist{#2}%
  \foreachitem\i\in\arglist{\expandafter#1\expandafter{\i}}%
}
\begin{document}
\newcommand{\ComplicatedCommand}[1]{\lowercase{#1}}
\apply{\ComplicatedCommand}{WHY,DO,THESE,WORDS,NOT,PRINT,IN,LOWERCASE?}

\newcommand{\MoreComplicatedCommand}[1]{\lowercase{\uppercase{{\lowercase{#1}}}}}
\apply{\MoreComplicatedCommand}{ULTIMATELY,I,WANT,APPLY,TO,MAKE,THESE,LOWERCASE,AS,WELL.}
\end{document}

enter image description here

Obviously, adding a space in the \foreachitem loop, as in

\foreachitem\i\in\arglist{\expandafter#1\expandafter{\i} }%

will add a space between looped output:

enter image description here

You must log in to answer this question.

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