2.7.5 ⁂Iteration Tips and Tricks
This section describes some advanced techniques that you may or may not need to know, so feel free to skip it.
Recall from §2.7.1 Iterating Through a Database I had:
which, when using the sample people.csv file, produced:
Suppose instead I wanted to produce:
If I try:
I get:
(there's an unwanted semi-colon and space before the terminating full stop) and if I try:
I get:
(The unwanted semi-colon and space are now at the start.)
Neither or these are quite right. Here's a way of achieving the desired output:
\newcommand{\surnamesep}{% \renewcommand{\surnamesep}{; }% }% \DTLforeach*{people}{\Surname=surname}{\surnamesep\Surname}.
This may look a bit weird at first sight, but here's how it works:
- On the first iteration
\surnamesep\Surnameis equivalent to
\renewcommand{\surnamesep}{; }\SurnameThat is,
\surnamesepis redefined to ;␣ (semicolon followed by a space) without displaying anything and the first surname is printed. - On the next iteration
\surnamesep\Surnameis equivalent to just
; \Surname(because
\surnamesephas just been redefined to ;␣) so a semi-colon followed by a space followed by the second surname is printed. Since\surnamesepdoesn't get redefined any more, it remains the same for the rest of the loop.
tabular environment, as in
Example 6) since \renewcommand only has a local
effect. Instead you can use TeX's \gdef command described in
§2.1.1 Macro Definitions:
\newcommand{\surnamesep}{% \gdef\surnamesep{; }% }% \DTLforeach*{people}{\Surname=surname}{\surnamesep\Surname}.
Alternatively you can do a global \let after you've
redefined the command:
\newcommand{\surnamesep}{% \renewcommand\surnamesep{; }% \global\let\surnamesep\surnamesep }% \DTLforeach*{people}{\Surname=surname}{\surnamesep\Surname}.
The datatool package defines the command:
designed for use within the ⟨body⟩ of \DTLforeach so
I could just do:
However, the technique described above can be used in more general
situations. For example, suppose I want to use etoolbox's
\docsvlist, described in §2.7.2 Iterating Over a Comma-Separated List, I could
do:
\newcommand{\surnamesep}{% \renewcommand{\surnamesep}{; }% }% \renewcommand\do[1]{\surnamesep#1}% \docsvlist{Parrot,Canary,Zebra,Arara,Duck}.
which produces:
The datatool package also defines the command:
As with \DTLiffirstrow, this command is designed for use within the
⟨body⟩ of \DTLforeach (or \DTLforeach*). For example:
\DTLforeach* {people}% database {\Surname=surname}% assignment list {% \DTLiffirstrow{}{\DTLiflastrow{ and }{, }}% \Surname }.
produces:
So how can we do the equivalent for a general comma-separated list rather
than using \DTLforeach? One possible method is described in
the example below.
This is slightly more complicated but it uses a similar technique to earlier:
\newcommand*{\surnamesep}{}% \newcommand*{\lastsurname}{}% \newcommand*{\prelastsurname}{}% \renewcommand*{\do}[1]{% \surnamesep \lastsurname \renewcommand{\lastsurname}{% \renewcommand{\surnamesep}{, }% \renewcommand{\prelastsurname}{ and }% #1% }}% \docsvlist{Parrot,Canary,Zebra,Arara,Duck}% \prelastsurname \lastsurname
This produces:
Here's how it works:
- On the first iteration (
\do{Parrot})\surnamesepand\lastsurnamedo nothing. Then\lastsurnameis redefined via:(The #1 has been replaced with the argument passed to\renewcommand{\lastsurname}{%
\renewcommand{%\surnamesep}{, }
\renewcommand{%\prelastsurname}{ and }
Parrot%
}
\do.) So far nothing has been displayed.If this was the only item in the list, the loop would end and then:
\prelastsurnamewould be done, but this is currently nothing.\lastsurnamewould be done, which is now set to redefine a couple of commands that are no longer needed (\surnamesepand\prelastsurname) and then displays “Parrot”.
- On the second iteration (
\do{Canary})\surnamesepstill does nothing but\lastsurnamenow does: So\surnamesepgets redefined to do ,␣ (that is a comma followed by a space) and\prelastsurnamegets redefined to do ␣and␣. After these redefinitions, the word “Parrot” is then displayed. Next\lastsurnameis redefined via:Therefore, by the end of the second iteration, the only text displayed is “Parrot”. If this happened to be the last item in the list, the loop would end and then:\renewcommand{\lastsurname}{%
\renewcommand{%\surnamesep}{, }
\renewcommand{%\prelastsurname}{ and }
Canary%
}
\prelastsurnamewould be done, which now displays “ and ”.\lastsurnamewould be done, which redefines some commands we no longer need (\surnamesepand\prelastsurname) and then displays “Canary”.
- On the third iteration (
\do{Zebra})\surnamesepnow displays a comma followed by a space and\lastsurnamedoes: Then\lastsurnameis redefined via:So we have thus far produced the text “Parrot, Canary”. If this happened to be the last item in the list, the loop would end and then:\renewcommand{\lastsurname}{%
\renewcommand{%\surnamesep}{, }
\renewcommand{%\prelastsurname}{ and }
Zebra%
}
\prelastsurnamewould display “ and ”\lastsurnamewould redefine some commands that we no longer need (\surnamesepand\prelastsurname) and then display “Zebra”.
If you plan to use this method more than once, you might prefer to define a new command. For example:
% set up defaults so we don't get an error % when we try to redefine these commands \newcommand*{\surnamesep}{}% \newcommand*{\lastsurname}{}% \newcommand*{\prelastsurname}{}% % define the new command to process a list of names: \newcommand*{\displaynames}[1]{% % initialise: \renewcommand*{\surnamesep}{}% \renewcommand*{\lastsurname}{}% \renewcommand*{\prelastsurname}{}% % set up list handler: \renewcommand*{\do}[1]{% \surnamesep \lastsurname \renewcommand{\lastsurname}{% \renewcommand{\surnamesep}{, }% \renewcommand{\prelastsurname}{ and }% ##1% }}% \docsvlist{#1}% \prelastsurname \lastsurname }
Since we have nested definitions of commands that take a parameter
we need to be careful how we reference the parameter. In the above
#1 refers to the argument of
\displaynames (the outer command) and
##1 refers to the argument of \do
(the inner command).
In this case, it's better to define your own handler macro and use
\forlistloop instead of \docsvlist:
% set up defaults so we don't get an error % when we try to redefine these commands \newcommand*{\surnamesep}{}% \newcommand*{\lastsurname}{}% \newcommand*{\prelastsurname}{}% % define the handler macro: \newcommand*{\dodisplayname}[1]{% \surnamesep \lastsurname \renewcommand{\lastsurname}{% \renewcommand{\surnamesep}{, }% \renewcommand{\prelastsurname}{ and }% #1% }}% % define the new command to process a list of names: \newcommand*{\displaynames}[1]{% % initialise: \renewcommand*{\surnamesep}{}% \renewcommand*{\lastsurname}{}% \renewcommand*{\prelastsurname}{}% % iterate through list: \forcsvlist{\dodisplayname}{#1}% % finish off: \prelastsurname \lastsurname }
This removes one of the nested redefinitions and the need to use ##1 instead of #1.
Some people always use the Oxford comma, some people never use it, and some people only use it where its lack would cause ambiguity. The purpose of this exercise is not to engage in a heated debate over whether or not it should be used. It's simply an exercise in iteration techniques. For those who've never heard of the Oxford comma, it's a comma that's placed after the penultimate item in a list of three or more items before the “and”. For example: Parrot, Canary, and Zebra.
For this exercise, see if you can adapt the definition
of \displaynames from the end of the previous example so
that it uses the Oxford comma. To test it:
\displaynames{Parrot,Canary,Zebra,Arara,Duck} \displaynames{Parrot,Canary,Zebra,Arara} \displaynames{Parrot,Canary,Zebra} \displaynames{Parrot,Canary} \displaynames{Parrot}
should produce:
This book is also available as A4 PDF or 12.8cm x 9.6cm PDF or paperback (ISBN 978-1-909440-07-4).
