5

I would like to define a macro for defining further macros to display sequences/lists/tuples of elements by specifying the following parameters:

  1. The name of the new command.
  2. What to display for an empty sequence/list/tuple.
  3. How to open a singleton sequence/list/tuple.
  4. How to close a singleton sequence/list/tuple.
  5. How to open a sequence/list/tuple with more than one element.
  6. How to close a sequence/list/tuple with more than one element.
  7. What to display as separating object between two elements in the sequence/list/tuple.

I had a look at the following questions and corresponding answers:

So I tried to code my command and came up with the following two versions (the second one is outcommented in the code below). Unfortunately, none of them works and I cannot figure out what the problem is:

\documentclass{article}

\makeatletter 
\newcommand{\definelistcommand}[7]{%
\expandafter\newcommand\csname @start#1\endcsname{\expandafter\@ifnextchar\csname @stop#1\endcsname{#2\csname @end#1\endcsname}{\csname @first#1\endcsname}}%
\expandafter\newcommand\csname @first#1\endcsname[1]{\expandafter\@ifnextchar\csname @stop#1\endcsname{#3##1#4\csname @end#1\endcsname}{#5##1\csname @next#1\endcsname}}%
\expandafter\newcommand\csname @next#1\endcsname[1]{#7##1\expandafter\@ifnextchar\csname @stop#1\endcsname{#6\csname @end#1\endcsname}{\csname @next#1\endcsname}}%
\expandafter\newcommand\csname @end#1\endcsname[1]{}% consumes the \stop command
\expandafter\newcommand\csname #1\endcsname[1]{\csname @start#1\endcsname##1\csname @stop#1\endcsname}%
}
\makeatother

% \makeatletter 
% \edef\definelistcommand#1#2#3#4#5#6#7{%
% \noexpand\newcommand\expandafter\noexpand\csname @start#1\endcsname{\noexpand\@ifnextchar\expandafter\noexpand\csname @stop#1\endcsname{#2\expandafter\noexpand\csname @end#1\endcsname}{\expandafter\noexpand\csname @first#1\endcsname}}%
% \noexpand\newcommand\expandafter\noexpand\csname @first#1\endcsname[1]{\noexpand\@ifnextchar\expandafter\noexpand\csname @stop#1\endcsname{#3##1#4\expandafter\noexpand\csname @end#1\endcsname}{#5##1\expandafter\noexpand\csname @next#1\endcsname}}%
% \noexpand\newcommand\expandafter\noexpand\csname @next#1\endcsname[1]{#7##1\noexpand\@ifnextchar\expandafter\noexpand\csname @stop#1\endcsname{#6\expandafter\noexpand\csname @end#1\endcsname}{\expandafter\noexpand\csname @next#1\endcsname}}%
% \noexpand\newcommand\expandafter\noexpand\csname @end#1\endcsname[1]{}% consumes the \stop command
% \noexpand\newcommand\expandafter\noexpand\csname #1\endcsname[1]{\expandafter\noexpand\csname @start#1\endcsname##1\expandafter\noexpand\csname @stop#1\endcsname}%
% }
% \makeatother

\definelistcommand{mylist}{empty}{[}{]}{(}{)}{,}

\begin{document}

\mylist{{a}{b}{c}}

\mylist{{a}{b}}

\mylist{a}

\mylist{{b}}

\mylist{}

\end{document}

The desired output would be:

(a,b,c)
(a,b)
[a]
[b]
empty

For the first version, I get this error message:

! Missing \endcsname inserted.
<to be read again>
                   \let
l.27 \mylist{{a}{b}{c}}

For the second version, I get this:

! Undefined control sequence.
l.27 \mylist
            {{a}{b}{c}}

What are the errors in my commands and how would a correct solution work?

EDIT (to point future readers to the fact that the accepted answer is not the only interesting one): Although I accepted the answer by Christian Hupfer (since it contains an explanation and fix for my attempt), the other answers also contain valuable contributions. In particular, I will use egreg's solution as it allows a key-value pair specification of my desired parameters. Unfortunately, I cannot accept two answers.

cryingshadow
  • 2,350
  • Wouldn't this be much easier with etoolbox \listadd etc. or with expl3 and its clist or seq features? –  Dec 03 '15 at 19:43
  • @ChristianHupfer I'm not very familiar with etoolbox. If you have a simpler solution with these, just put that as an answer. – cryingshadow Dec 03 '15 at 19:44
  • I'll try, but I am not sure about your purposes. You want a mapper, basically? –  Dec 03 '15 at 19:52
  • I have several list-like structures and would like to have one command for each of these structures by which I can easily define how these structures are displayed (e.g., a sequence of substitutions should be displayed differently than a sequence of states, but each sequence of substitutions should be displayed in the same format). Since the list-like structure is what these structures have in common, I would like to define one generic command for all of them rather than defining several commands in more or less the same way. – cryingshadow Dec 03 '15 at 20:01
  • While there are already good solutions how to define the macro by different approaches, I would still like to know why my attempts fail and how to fix them. Does somebody have an idea? – cryingshadow Dec 04 '15 at 08:44
  • I think the error is \expandafter\@ifnextchar\csname \@stop#1\endcsname -- it does not expand correctly. Do you really need a \@stop#1 or will a general \@stopthis do as well? (It works with \@stopthis, by the way. –  Dec 04 '15 at 12:32
  • 1
    I've added a working version with a global \@stoplist command at the end of my solution –  Dec 04 '15 at 12:38

3 Answers3

9

You can try this macro:

\newcount\tmpnum
\def\definelistcommand#1#2#3#4#5#6#7{%
   \expandafter\def\csname\string#1:list\endcsname##1{%
      \ifcase##1 #2\or#3\or#4\or#5\or#6\or#7\fi}%
   \def#1{\dolist#1}%
}
\def\dolist#1#2{\tmpnum=0 \def\tmp{}%
   \def\listcommand{\csname\string#1:list\endcsname}%
   \dolistA#2\end}
\def\dolistA#1{%
   \ifx\end#1%
      \ifnum\tmpnum=0 \listcommand0%
      \else\ifnum\tmpnum=1 \listcommand1\tmp\listcommand2%
      \else \listcommand3\tmp\listcommand4%
      \fi\fi
   \else\edef\tmp{\ifx\tmp\empty\else\tmp\listcommand5\fi#1}%
      \advance\tmpnum by1
      \expandafter\dolistA\fi
}

\definelistcommand \mylist {empty} [](),

\mylist{{a}{b}{c}}

\mylist{{a}{b}}

\mylist{a}

\mylist{{b}}

\mylist{}

Explanation. The \definelistcommand\foo defines two control sequences. First one \\foo:list as \ifcase. It expands the appropriate declared parameter: \\foo:list0 expnads to nothing, \\foo:list1 to [ etc. Second defined control sequence is \foo. This is macro \dolist\foo.

The usage of \foo{text} expands to \dolist\foo{text}. This macro resets \tmp to empty, \tmpnum to zero and saves \listcommand as \\foo:list. Then it runs \dolistA text\end.

The \dolistA reads the items of the text repeatedly. It saves these items to the \tmp macro using \edef\tmp. If \tmp is not empty then the comma (alias \listcommand5) is added between the previous contents and the next item #1. The \tmpnum counts the number of items. When \end is reached then \tmp is printed depending on the \tmpnum value.

Note that we need not any external package and we can declare the list-like macro very simply.

wipet
  • 74,238
  • Thanks, that works. Could you explain a little bit how it works? It seems you are using a counter for the number of elements and that several further commands are defined which might have conflicting names with other user-defined commands. – cryingshadow Dec 03 '15 at 20:12
  • 1
    @cryingshadow If there will be more votes then I'll add the explanation. I see more votes in LaTeX 3 solution (which is absurd language), but solutions based only on classical TeX is not evaluated here. And to conflicting names with other user-defined commands: what is \hbox, \vskip etc? They are conflicting names with user-defined commands too. – wipet Dec 03 '15 at 22:19
  • I can only vote once... ;) I am just interested in how it works and what to keep in mind when using it (that is what the part on conflicting names is about - it was not intended as a criticism against your solution). – cryingshadow Dec 03 '15 at 22:25
6

Rather than a long list of arguments, I think it's better to use a key-value syntax.

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\definelistcommand}{mm}
 {% #1 is the command name, #2 is the key-value setup
  % clear the temporary property list
  \prop_clear:N \l_cryingshadow_list_temp_prop
  \keys_set:nn { cryingshadow/list } { #2 }
  % check if left-single or right-single have been set
  \prop_if_in:NnF \l_cryingshadow_list_temp_prop { left-single }
   {
    \prop_put:Nnx \l_cryingshadow_list_temp_prop { left-single }
     { \prop_item:Nn \l_cryingshadow_list_temp_prop { left } }
   }
  \prop_if_in:NnF \l_cryingshadow_list_temp_prop { right-single }
   {
    \prop_put:Nnx \l_cryingshadow_list_temp_prop { right-single }
     { \prop_item:Nn \l_cryingshadow_list_temp_prop { right } }
   }
  % allocate a specific property list and make it equal to the temporary one
  \prop_new:c { g_cryingshadow_list_#1_prop }
  \prop_gset_eq:cN { g_cryingshadow_list_#1_prop } \l_cryingshadow_list_temp_prop
  % define the runtime macro to call the generic one with the current name as argument
  \cs_new_protected:cpn { #1 } ##1 { \cryingshadow_list_print:nn { #1 } { ##1 } }
 }

% allocate some variables
\prop_new:N \l_cryingshadow_list_temp_prop
\seq_new:N \l_cryingshadow_list_input_seq

% syntactic sugar
\cs_new_protected:Nn \__cryingshadow_list_put:nn
 {
  \prop_put:Nnn \l_cryingshadow_list_temp_prop { #1 } { #2 }
 }
\cs_new_protected:Nn \__cryingshadow_list_item:nn
 {
  \prop_item:cn { g_cryingshadow_list_#1_prop } { #2 }
 }

% define the keys
\keys_define:nn { cryingshadow/list }
 {
  empty .code:n = \__cryingshadow_list_put:nn { empty } { #1 },
  left-single .code:n = \__cryingshadow_list_put:nn { left-single } { #1 },
  right-single .code:n = \__cryingshadow_list_put:nn { right-single } { #1 },
  left .code:n = \__cryingshadow_list_put:nn { left } { #1 },
  right .code:n = \__cryingshadow_list_put:nn { right } { #1 },
  delimiter .code:n = \__cryingshadow_list_put:nn { delimiter } { #1 },
 }

\cs_new_protected:Nn \cryingshadow_list_print:nn
 {
  % split the input into items at commas
  \seq_set_split:Nnn \l_cryingshadow_list_input_seq { , } { #2 }
  % branch between empty or single and multiple items
  \int_compare:nTF { \seq_count:N \l_cryingshadow_list_input_seq = 1 }
   {
    % if the sequence has just one item it could be empty
    \tl_if_blank:nTF { #2 }
     { \__cryingshadow_list_item:nn { #1 } { empty } }
     {
      \__cryingshadow_list_item:nn { #1 } { left-single }
      #2
      \__cryingshadow_list_item:nn { #1 } { right-single }
     }
   }
   {
    % otherwise it has multiple items
    \__cryingshadow_list_item:nn { #1 } { left }
    \seq_use:Nn \l_cryingshadow_list_input_seq { \__cryingshadow_list_item:nn { #1 } { delimiter } }
    \__cryingshadow_list_item:nn { #1 } { right }
   }
 }

\ExplSyntaxOff

\definelistcommand{firstlist}{
  empty=empty,
  left-single=[,
  right-single=],
  left=(,
  right=),
  delimiter={, },
}
\definelistcommand{secondlist}{
  empty=nothing,
  left=\{,
  right=\},
  delimiter={--},
}

\begin{document}

\firstlist{A,B,C}

\firstlist{A,B}

\firstlist{a}

\firstlist{}

\secondlist{A,B,C}

\secondlist{A,B}

\secondlist{a}

\secondlist{}

\end{document}

enter image description here

How does this work?

The \definelistcommand first sets up a property list specific for the list command to be defined, where to store the various items (outer delimiters, inner delimiters, what to do for the empty case); then it defines the macro to call a generic function for producing the output.

The property list is supplied the values by examining the second argument. If left-single or right-single are not specified, the values for left or right are supplied.

At call time, the generic macro takes the values from the specific property list, splits the argument at commas and then branches: if the argument is blank, the value for the case is printed; if there's only one item, the left-single and right-single outer delimiters are used; otherwise the left and right ones, with the inner delimiter between items.

egreg
  • 1,121,712
  • I really like the idea of key-value pairs. Seems I have to dive into xparse and friends for sophisticated macros... – cryingshadow Dec 03 '15 at 21:31
  • 1
    @cryingshadow You can also easily supplement the list of keys, for instance you can add a key for the list separator at run time; or maybe define a constant property list with default values, so you can specify only the ones that are to be changed; you can add a check for the “single” keys and, if missing, use the “nonsingle” ones. – egreg Dec 03 '15 at 21:35
  • @cryingshadow I made some changes along the lines of my previous comment. – egreg Dec 03 '15 at 21:51
  • I just noted one thing in this solution that is problematic in my setting: It seems that this command "breaks out" of math mode. So I cannot just use it within math mode as I can, e.g., with the solution by @wipet. – cryingshadow Dec 03 '15 at 22:02
  • @cryingshadow I have no problem and, no, it doesn't do differently in text or math mode; of course, if you plan to use \langle and \rangle as outer delimiters and the macro in math mode, you shouldn't say left=$\langle$, but left=\langle (or left=\ensuremath{\langle}). – egreg Dec 03 '15 at 22:05
  • Never mind. I had a different error and just the error message was confusing. Yes, your solution also works perfectly in math mode. – cryingshadow Dec 03 '15 at 22:12
5

Here's a expl3 version with seq variables.

The command \NewListCommand declares the command list macro named like the first argument and uses a global seq variable with some prefix, see \g_cryingshadow_mynewlist_seq etc.

This is filled by the \mynewlist command and the content displayed depending on the content of the other parameters from #3 to #6. The 7th. parameter is used as a delimiter

Some notes: The empty parameter could be an optional one.

\documentclass{article}


\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\NewListCommand}{mmmmmmm}{%
  \seq_new:c {g_cryingshadow_#1_seq} % Make a new sequence
  \expandafter\NewDocumentCommand\csname #1\endcsname{m}{%
    \seq_gclear:c {g_cryingshadow_#1_seq}% Clear the sequence
    \seq_gclear:N \l_tmpa_seq % Clear a temporary `\l_tmpa_seq
    \seq_set_split:Nnn \l_tmpa_seq {} {##1} % Split the sequence into tokens. 
    \seq_set_eq:cN {g_cryingshadow_#1_seq} \l_tmpa_seq % copy the temporary sequence to the real one → this would not be necessary if the sequence is not be used outside 
    \seq_if_empty:cTF {g_cryingshadow_#1_seq} {%
      #2% Checked --> is empty
    }{%
      % Display the various styles
      \int_case:nn { \seq_count:c {g_cryingshadow_#1_seq} }
      {%
        {1} {#3      \seq_use:cnnn {g_cryingshadow_#1_seq} {,} {#7} {#7} #4}
      }%
      \int_compare:nNnT { \seq_count:c {g_cryingshadow_#1_seq}}  > 1 {%
        #5      \seq_use:cnnn {g_cryingshadow_#1_seq} {#7} {#7} {#7} #6
      }
    }
  }
}
\ExplSyntaxOff


\NewListCommand{mynewlist}{empty}{[}{]}{(}{)}{,}


\begin{document}

\mynewlist{{A}{B}{C}}

\mynewlist{{A}{B}}

\mynewlist{{a}}

\mynewlist{}


\mynewlist{{A}{B}{C}{D}{E}{F}}


Astronomer's alphabet:

\mynewlist{{Oh}{be}{a}{fine}{girl}{kiss}{me}}

\end{document}

enter image description here

Edit: Here's the working version for usual LaTeX:

\documentclass{article}
\usepackage{etoolbox}

\makeatletter

\def\definelistcommand#1#2#3#4#5#6#7{%

  \expandafter\def\csname @firstinlist#1\endcsname##1{%
    \@ifnextchar\@stoplist{%
      #3##1#4\@endlist%
    }{%
      #5##1\csname @nextlist#1\endcsname%
    }%
  }

  \expandafter\def\csname @nextlist#1\endcsname##1{%
    \@ifnextchar\@stoplist{%
      #7##1#6\@endlist%
    }{%
      #7##1\csname @nextlist#1\endcsname%
    }%
  }


  \expandafter\def\csname @startlist#1\endcsname##1{%
    \@ifnextchar\@stoplist{%
      #3##1#4\@endlist%
    }{%
      \csname @firstinlist#1\endcsname##1% 
    }%
  }

  \expandafter\def\csname #1\endcsname##1{%
    \ifblank{##1}{%
      #2%
    }{%
      \csname @startlist#1\endcsname##1\@stoplist%
    }%
  }
}

\def\@endlist#1{}


\definelistcommand{mycmd}{empty}{[}{]}{(}{)}{,}

\definelistcommand{othercmd}{empty}{\textbraceleft}{\textbraceright}{(}{)}{+}

\begin{document}


\makeatother

\mycmd{{A}{B}{C}}

\mycmd{A}

\mycmd{{A}{B}}

\mycmd{}

\othercmd{{O}{B}}

\othercmd{{O}}


\end{document}
  • Thanks! Could you explain your solution a little bit? I'm not very familiar with xparse etc. and would like to get an idea how and why this works as it does (and maybe then also get an idea why my solution does not). – cryingshadow Dec 03 '15 at 20:37
  • 1
    @cryingshadow: I wished I could explain why your versions do not work, at the moment I can't. I'll add some explanations about the code into the code –  Dec 03 '15 at 20:40
  • 1
    @cryingshadow: I'll use xparse and expl3 very frequently, although I am no expert of course -- but I can recommend it, xparse itself is easy to learn –  Dec 03 '15 at 20:48
  • @cryingshadow: You constantly call \csname @stop#1\endcsname This is never defined (so the result is \relax.) –  Dec 03 '15 at 21:17
  • True, but it should never be executed - it should be consumed by the @end#1 command. – cryingshadow Dec 03 '15 at 21:19
  • @cryingshadow: After looking into your code again, I think that your code does not look far enough to see the \@stop etc. really –  Dec 03 '15 at 22:04
  • What do you mean by "does not look far enough" and how could that be fixed? – cryingshadow Dec 03 '15 at 22:15