The \foreach
loops execute their code in a group for each step, so they are useless when something must be changed "globally", as is the case for your databases.
Here's a way using the patch proposed by Werner; but I used a different command to patch, in order not to change the functionality of \DTLgetlocation
:
\documentclass{article}
\usepackage[paperheight=15in]{geometry}% so output fits on "one" page
\usepackage{datatool}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\foreachin}{ m +m } % long arguments
{
\clist_map_inline:nn { #1 } { #2 }
}
\ExplSyntaxOff
%%% Werner's patch
\usepackage{etoolbox}
\makeatletter
% Patch \DTLgetlocation to gobble the error
\let\ErrorFreeDTLgetlocation\DTLgetlocation
\patchcmd{\ErrorFreeDTLgetlocation}% <cmd>
{\PackageError}% <search>
{\@gobbletwo}% <replace>
{}{}% <success><failure>
\def\@@dtlnovalue{\@dtlnovalue}
%%%
\newcommand*{\DBKey}{Value}
\newcommand*{\InitalizeDuplicatesDB}[1]{%
% #1 = name to be used for this duplicate's DB
\DTLifdbexists{#1}%
{\DTLcleardb{#1}}% DB exists, so just clear it
{\DTLnewdb{#1}}% DB does not exist, so create it
}
\newcommand*{\AddMemberToDB}[2]{%
% #1 = name of DB
% #2 = member to be added to DB
\DTLnewrow{#1}%
\DTLnewdbentry{#1}{\DBKey}{#2}%
}
\newcommand*{\IfIsInDB}[4]{% %New definition!
% #1 = name to be used for this duplicate's DB
% #2 = member to check if in DB (gets added if not)
% #3 = code to execute if member is in DB
% #4 = code to execute if member is not in DB
\ErrorFreeDTLgetlocation{\RowIndex}{\ColIndex}{#1}{#2}%
%row=\RowIndex \quad col=\ColIndex\par
\ifx\RowIndex\@@dtlnovalue
#4%
\AddMemberToDB{#1}{#2}%
\else
#3
\fi
}
\makeatother
\begin{document}
\InitalizeDuplicatesDB{duplicates}
\AddMemberToDB{duplicates}{ABC}
\AddMemberToDB{duplicates}{DEF}
Current db:\par
\DTLdisplaydb{duplicates}
\medskip
\IfIsInDB{duplicates}{ABC}{ABC is a duplicate}{ABC added DB}\par
\IfIsInDB{duplicates}{DEF}{DEF is a duplicate}{DEF added DB}\par
\IfIsInDB{duplicates}{XYZ}{XYZ is a duplicate}{XYZ added DB}\par
\medskip
Current db (before foreach - no macro):\par
\DTLdisplaydb{duplicates}
% Following tests added in update.
\medskip\par\noindent
Testing with a non-macro defined list (ABC, DEF should not be ``added", and MMM should only be ``added" once):\par
\foreachin {ABC, DEF, ABC, MMM, MMM, JHM}{%
\IfIsInDB{duplicates}{#1}{#1~is a duplicate}{#1~added DB}\par
}
Current db (after foreach - no macro):\par
\DTLdisplaydb{duplicates}
\end{document}
As you see, the syntax of \foreachin
is very similar to \foreach\x in
; instead of \x
you use #1
in the code part.
A variant
If you want to be able to pass the list to \foreachin
as a macro, say
\newcommand{\mylist}{ABC, DEF, ABC, MMM, MMM, JHM}
\foreachin{\mylist}{\IfIsInDB{duplicates}{#1}{#1~is a duplicate}{#1~added DB}}
then change the definition of \foreachin
:
\ExplSyntaxOn
\NewDocumentCommand{\foreachin}{ m +m } % long arguments
{
\clist_map_inline:on { #1 } { #2 }
}
\cs_generate_variant:Nn \clist_map_inline:nn {o}
\ExplSyntaxOff
A completely different approach
Instead of datatool
you might use the property list data type of expl3
.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\InitializeDuplicatesDB}{m}
{
\prop_gclear_new:c { g_grill_#1_prop }
}
\NewDocumentCommand{\AddMemberToDB}{ m m }
% #1 = name of DB
% #2 = member to be added to DB
{
\prop_gput:cnn { g_grill_#1_prop } { #2 } { Value }
}
\NewDocumentCommand{\IfIsInDB}{mmmm}
% #1 = name to be used for this duplicate's DB
% #2 = member to check if in DB (gets added if not)
% #3 = code to execute if member is in DB
% #4 = code to execute if member is not in DB
{
\prop_get:cnNTF { g_grill_#1_prop } { #2 } \l_tmpa_tl
{ #3 }
{ \AddMemberToDB{#1}{#2} #4 }
}
\NewDocumentCommand{\DTLdisplaydb}{m}
{
\prop_map_inline:cn { g_grill_#1_prop } { ##1\par }
}
\ExplSyntaxOff
\begin{document}
\InitializeDuplicatesDB{duplicates}
\AddMemberToDB{duplicates}{ABC}
\AddMemberToDB{duplicates}{DEF}
Current db:\par
\DTLdisplaydb{duplicates}
\medskip
\IfIsInDB{duplicates}{ABC}{ABC is a duplicate}{ABC added DB}\par
\IfIsInDB{duplicates}{DEF}{DEF is a duplicate}{DEF added DB}\par
\IfIsInDB{duplicates}{XYZ}{XYZ is a duplicate}{XYZ added DB}\par
\medskip
Current db (before foreach - no macro):\par
\DTLdisplaydb{duplicates}
\end{document}
The for each
cycle can then be defined just as before. Of course, in a real application, you'll add an argument to \AddMemberToDB
(to replace the common Value
given to each key).
And now for something completely different
If what you really need is to maintain a comma separated list, adding one element at a time unless it's already present, you can do with the clist data type of expl3
.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\NewList}{ m }
{
\clist_new:c { g_pg_#1_clist }
}
\NewDocumentCommand{\AddToList}{ m m }
{
\clist_if_in:cnTF { g_pg_#1_clist } { #2 }
{ #2~is~a~duplicate~in~\texttt{#1}\par }
{
\clist_gput_right:cn { g_pg_#1_clist } { #2 }
#2~added~to~\texttt{#1}\par
}
}
\NewDocumentCommand{\ShowList}{ m }
{
List~\texttt{#1}~contains\par
\clist_map_inline:cn { g_pg_#1_clist } { ##1\par }
}
\NewDocumentCommand{\foreachin}{ m m } % long arguments
{
\clist_map_inline:Vn #1 { #2 }
}
\cs_generate_variant:Nn \clist_map_inline:nn {V}
\ExplSyntaxOff
\setlength{\parindent}{0pt}
\begin{document}
\NewList{duplicates} % allocate a new list
\AddToList{duplicates}{ABC}
\AddToList{duplicates}{DEF}
\ShowList{duplicates}
\AddToList{duplicates}{ABC}
\AddToList{duplicates}{XYZ}
\ShowList{duplicates}
\newcommand{\mylist}{ABC, DEF, ABC, MMM, MMM, JHM}
\foreachin{\mylist}{\AddToList{duplicates}{#1}}
\ShowList{duplicates}
\end{document}
The output will be

If you don't care about doing something if the added entries are duplicate and want only to have uniqueness, you can use a simpler strategy:
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\NewList}{ m }
{
\clist_new:c { g_pg_#1_clist }
}
\NewDocumentCommand{\AddToList}{ s m m }
{
\IfBooleanTF{#1}
{
\clist_gput_right:cV { g_pg_#2_clist } #3
}
{
\clist_gput_right:cn { g_pg_#2_clist } { #3 }
}
\clist_gremove_duplicates:c { g_pg_#2_clist }
}
\NewDocumentCommand{\ShowList}{ m }
{
List~\texttt{#1}~contains\par
\clist_map_inline:cn { g_pg_#1_clist } { ##1\par }
}
\begin{document}
\NewList{duplicates}
\AddToList{duplicates}{ABC}
\AddToList{duplicates}{DEF}
\ShowList{duplicates}
\AddToList{duplicates}{ABC}
\AddToList{duplicates}{XYZ}
\ShowList{duplicates}
\newcommand{\mylist}{ABC, DEF, ABC, MMM, MMM, JHM}
\AddToList*{duplicates}{\mylist}
\ShowList{duplicates}
\end{document}
The output will be

To be on the safe side, the macro \AddToList
has a *-variant for giving a macro as the second argument.