3

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.

2

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}
  • 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. – David Carlisle Jan 11 '18 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. – Andrew Critch Jan 11 '18 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.

  • There is also \forcsvlist which is even closer to \apply and \DeclareListParser*. – moewe Jan 11 '18 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. – Andrew Critch Jan 11 '18 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

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.