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\Surname
is equivalent to
\renewcommand{\surnamesep}{; }\Surname
That is,
\surnamesep
is redefined to ;␣ (semicolon followed by a space) without displaying anything and the first surname is printed. - On the next iteration
\surnamesep\Surname
is equivalent to just
; \Surname
(because
\surnamesep
has just been redefined to ;␣) so a semi-colon followed by a space followed by the second surname is printed. Since\surnamesep
doesn'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}
)\surnamesep
and\lastsurname
do nothing. Then\lastsurname
is redefined via:\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:
\prelastsurname
would be done, but this is currently nothing.\lastsurname
would be done, which is now set to redefine a couple of commands that are no longer needed (\surnamesep
and\prelastsurname
) and then displays “Parrot”.
- On the second iteration (
\do
{Canary}
)\surnamesep
still does nothing but\lastsurname
now does: So\surnamesep
gets redefined to do ,␣ (that is a comma followed by a space) and\prelastsurname
gets redefined to do ␣and␣. After these redefinitions, the word “Parrot” is then displayed. Next\lastsurname
is redefined via:\renewcommand
{
\lastsurname
}{%
\renewcommand
{
%\surnamesep
}{, }
\renewcommand
{
%\prelastsurname
}{ and }
Canary%
}
\prelastsurname
would be done, which now displays “ and ”.\lastsurname
would be done, which redefines some commands we no longer need (\surnamesep
and\prelastsurname
) and then displays “Canary”.
- On the third iteration (
\do
{Zebra}
)\surnamesep
now displays a comma followed by a space and\lastsurname
does: Then\lastsurname
is redefined via:\renewcommand
{
\lastsurname
}{%
\renewcommand
{
%\surnamesep
}{, }
\renewcommand
{
%\prelastsurname
}{ and }
Zebra%
}
\prelastsurname
would display “ and ”\lastsurname
would redefine some commands that we no longer need (\surnamesep
and\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).