我编写了一个宏来减去两个列表,它实现了预期的功能。然后,我在 TeX.SE 上找到了一个设置,其中包含宏\g@addto@macro
,\@gobble
它更简单,并且原则上给出相同的输出。
在尝试使用第二种形式时,确实出现了一些问题。经过一番搜索,发现\@gobble
尚未展开,在列表中添加了一个额外的元素并造成了麻烦。
这个问题的最佳解决办法是什么?
我认为最好对结果进行处理,\processedlist
以便它可以用于任何需要列表的宏。然后,我们再次得到与第一个宏类似的结果,很多\expandafter
s,尽管这一次它们只会使用一次,因此应该会更快。
\documentclass{article}
\setlength{\parindent}{0pt}
\setlength{\parskip}{1.5ex plus 0.5ex minus 0.2ex}
\begin{document}
\makeatletter
\section*{First macro for subtracting two lists}
\newif\iffirstterm
\newif\ifisinlist
\def\subtractlists#1#2{% original list, remove list
\def\processedlist{}%
\firsttermtrue%
\@for\tempa:=#1\do{%
\isinlistfalse%
\@for\tempb:=#2\do{\ifx\tempa\tempb\isinlisttrue\fi}%
\ifisinlist%
\else%
\iffirstterm%
\expandafter\def\expandafter\processedlist\expandafter{\tempa}%
\firsttermfalse%
\else%
\expandafter\expandafter\expandafter\def%
\expandafter\expandafter\expandafter\processedlist%
\expandafter\expandafter\expandafter{%
\expandafter\processedlist\expandafter,\tempa}%
\fi%
\fi}%
}
%%% some lists for testing:
\def\originallist{Lima,Alpha,Delta,Oscar,Tango,Whisky,Echo,Romeo,Xray}
\def\removelist{Whisky,Oscar,Romeo,Delta}
%The list can be entered as comma separated items or as a macro:
\verb|\originallist|: \originallist\par
\verb|\removelist|: \removelist\par
\verb|\subtractlists{\originallist}{\removelist}|\par
\subtractlists{\originallist}{\removelist}
\verb|\processedlist|: \processedlist\par
%\@for\tempa:=\processedlist\do{(\tempa)}
\section*{Second macro for subtracting two lists}
\def\subtractlist#1#2{% #1:original list,#2:remove list
\def\processedlist{\@gobble}%
\@for\tempa:=#1\do{%
\isinlistfalse%
\@for\tempb:=#2\do{\ifx\tempa\tempb\isinlisttrue\fi}%
\ifisinlist%
\else%
\expandafter\g@addto@macro%
\expandafter\processedlist%
\expandafter{\expandafter,\tempa}%
\fi}%
}
\verb|\originallist|: \originallist\par
\verb|\removelist|: \removelist\par
\verb|\subtractlist{\originallist}{\removelist}|\par
\subtractlist{\originallist}{\removelist}
\verb|\processedlist|: \processedlist\par
\section*{Problems with second approach}
There is some extra matter:
\@for\tempa:=\processedlist\do{(\tempa)}
the first comma has not been gobbled yet:
\verb|\meaning\processedlist|: \meaning\processedlist
\section*{What is the solution?}
This works:
\expandafter\@for\expandafter\tempa%
\expandafter:\expandafter=\processedlist\do{(\tempa)}
but maybe this is safer?:
\expandafter\@for\expandafter\tempa%
\expandafter:\expandafter=%
\expandafter{\processedlist}\do{(\tempa)}
However, the objective is to have \verb|\processedlist| clean of the extra
matter to use it as list in some other macro. Maybe:
\expandafter\expandafter\expandafter\def%
\expandafter\expandafter\expandafter\processedlist%
\expandafter\expandafter\expandafter{\processedlist}
\meaning\processedlist
which is more or less what we had in the first macro.
\end{document}
答案1
正如 Andrew 所说,expl3
有很多高级工具可用于处理很多事情,包括列表处理。但expl3
不管怎样,最重要的是了解你在做什么,并发布一个最低限度的工作例如当您遇到困难时(您的代码没有\end{document}
)。
因此,我将重点介绍您的“第二种方法”。您使用第一个项目初始化结果\@gobble
,并期望它会神奇地消失?为什么会这样?事实并非如此,这就是\meaning
向您展示的。这个想法可能是使用类似的东西来完成结果\xdef\processedlist{\processedlist}
,以便\@gobble
吃掉后面的逗号,但这非常丑陋,因为\xdef
(如\edef
)在扩展来自真实项目(如\textbf
,\ref
等)的标记时可能会导致严重的问题。\protected@xdef
问题会少一些,但它仍然会毫无理由地扩展所有项目中的内容。
在这里,由于项目仅由非活动字符标记组成,因此这种方法是可行的,但这会破坏更精细的输入,产生不良的副作用,因此在我看来是一种不好的做法。我的建议是\@gobble
根本不要添加该项目,而只是检查是否\processedlist
为空,以决定是否在新添加的项目中添加逗号。
\@gobble
话虽如此,我还是会在最后给出一种没有不良副作用的方法来处理。它比下面的代码要长一点,但应该会稍微快一点,因为用于将项目添加到的代码\processedlist
少了一个检查。
无论使用什么技巧去掉第一个逗号,该算法都是相对低效的,因为即使内层循环已经找到了#1
中的当前项#2
,它仍会继续检查的所有剩余项#2
。为了避免这个问题,它expl3
使用了\prg_break_point:
和\prg_break:
,更重要的是,支持这种循环中断的映射函数(例如\clist_map_inline:nn
和\clist_map_break:
)。
\documentclass{article}
\usepackage[T1]{fontenc} % for the '>' character in horizontal mode
\makeatletter
\newif\ifisinlist
\def\subtractlist#1#2{% #1:original list, #2:remove list
\def\processedlist{}%
\@for\tempa:=#1\do{%
\isinlistfalse
\@for\tempb:=#2\do{\ifx\tempa\tempb\isinlisttrue\fi}%
\ifisinlist
\else
\ifx\processedlist\empty
\expandafter\gdef\expandafter\processedlist\expandafter{\tempa}%
\else
\expandafter\g@addto@macro
\expandafter\processedlist
\expandafter{\expandafter,\tempa}%
\fi
\fi
}%
}
\makeatother
\setlength{\parindent}{0pt}
\setlength{\parskip}{1.5ex plus 0.5ex minus 0.2ex}
\begin{document}
\section*{Second macro for subtracting two lists}
\def\originallist{Lima,Alpha,Delta,Oscar,Tango,Whisky,Echo,Romeo,Xray}
\def\removelist{Whisky,Oscar,Romeo,Delta}
\verb|\originallist|: \originallist\par
\verb|\removelist|: \removelist\par
\verb|\subtractlist{\originallist}{\removelist}|\par
\subtractlist{\originallist}{\removelist}
\verb|\processedlist|: \processedlist\par
\meaning\processedlist
\end{document}
随着\@gobble
正如承诺的那样,这里有一种使用初始值正确执行操作的方法\@gobble
:
\documentclass{article}
\usepackage[T1]{fontenc} % for the '>' character in horizontal mode
\makeatletter
\newif\ifisinlist
\def\startingPoint{\@gobble}
\def\subtractlist#1#2{% #1:original list, #2:remove list
\global\let\processedlist\startingPoint
\@for\tempa:=#1\do{%
\isinlistfalse
\@for\tempb:=#2\do{\ifx\tempa\tempb\isinlisttrue\fi}%
\ifisinlist
\else
\expandafter\g@addto@macro
\expandafter\processedlist
\expandafter{\expandafter,\tempa}%
\fi
}%
\ifx\processedlist\startingPoint
\gdef\processedlist{}%
\else
% Expand twice so that the \@gobble eats the comma that follows it.
\xdef\processedlist{%
\unexpanded\expandafter\expandafter\expandafter{\processedlist}}%
\fi
}
\makeatother
\setlength{\parindent}{0pt}
\setlength{\parskip}{1.5ex plus 0.5ex minus 0.2ex}
\begin{document}
\section*{Second macro for subtracting two lists}
\def\originallist{Lima,Alpha,Delta,Oscar,Tango,Whisky,Echo,Romeo,Xray}
\def\removelist{Whisky,Oscar,Romeo,Delta}
\verb|\originallist|: \originallist\par
\verb|\removelist|: \removelist\par
\verb|\subtractlist{\originallist}{\removelist}|\par
\subtractlist{\originallist}{\removelist}
\verb|\processedlist|: \processedlist\par
\meaning\processedlist
\end{document}
与上面相同的输出。
注意:如果您不理解下面这段代码:
\xdef\processedlist{%
\unexpanded\expandafter\expandafter\expandafter{\processedlist}}%
它的作用与以下相同:
\expandafter\expandafter
\expandafter\gdef
\expandafter\expandafter
\expandafter\processedlist
\expandafter\expandafter
\expandafter{\processedlist}%
PS:我发现简单的代码比用于完成非常简单的事情的expl3
一堆代码更具可读性。\expandafter
答案2
这本质上是expl3
开箱即用的。其思想是映射原始列表;如果某个项目不在删除列表中,则将其添加到临时列表中,最后将原始列表重置为临时列表。
我在名称列表中添加了一些语法糖,这样就可以拥有许多名称列表而无需定义新的宏。
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\definelist}{mm}
{% #1 = list name, #2 = items
% allocate a new list or clear an existing one
\clist_clear_new:c { l_kessels_#1_clist }
% set the list
\clist_set:cn { l_kessels_#1_clist } { #2 }
}
\NewDocumentCommand{\subtractlist}{mm}
{% #1 = original, #2 = items to subtract
\kessels_list_subtract:nn { #1 } { #2 }
}
\NewDocumentCommand{\uselist}{mm}
{% #1 = list name, #2 = output separator
\clist_use:cn { l_kessels_#1_clist } { #2 }
}
\clist_new:N \l__kessels_list_temp_clist
\cs_new_protected:Nn \kessels_list_subtract:nn
{
% clear the temporary list
\clist_clear:N \l__kessels_list_temp_clist
% map the original list
\clist_map_inline:cn { l_kessels_#1_clist }
{
% if the item doesn't appear in the remove list, add it to the temp one
\clist_if_in:cnF { l_kessels_#2_clist } { ##1 }
{
\clist_put_right:Nn \l__kessels_list_temp_clist { ##1 }
}
}
% reconstitute the original list
\clist_set_eq:cN { l_kessels_#1_clist } \l__kessels_list_temp_clist
}
\ExplSyntaxOff
\begin{document}
\definelist{original}{Lima,Alpha,Delta,Oscar,Tango,Whisky,Echo,Romeo,Xray}
\definelist{remove}{Whisky,Oscar,Romeo,Delta}
\uselist{original}{, }
\subtractlist{original}{remove}
\uselist{original}{, }
\end{document}
人们可以考虑添加\copylist
保存列表的副本并\maplist
定义对项目的操作。
更完整的实现。的第二个参数\maplist
是一个将每个项目作为的模板#1
。
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\definelist}{mm}
{% #1 = list name, #2 = items
% allocate a new list or clear an existing one
\clist_clear_new:c { l_kessels_#1_clist }
% set the list
\clist_set:cn { l_kessels_#1_clist } { #2 }
}
\NewDocumentCommand{\prependtolist}{mm}
{
\clist_put_left:cn { l_kessels_#1_clist } { #2 }
}
\NewDocumentCommand{\appendtolist}{mm}
{
\clist_put_right:cn { l_kessels_#1_clist } { #2 }
}
\NewDocumentCommand{\subtractlist}{mm}
{% #1 = original, #2 = items to subtract
\kessels_list_subtract:nn { #1 } { #2 }
}
\NewDocumentCommand{\uselist}{mm}
{% #1 = list name, #2 = output separator
\clist_use:cn { l_kessels_#1_clist } { #2 }
}
\NewDocumentCommand{\copylist}{mm}
{
\clist_clear_new:c { l_kessels_#2_clist }
\clist_set_eq:cc { l_kessels_#2_clist } { l_kessels_#1_clist }
}
\NewDocumentCommand{\maplist}{mm}
{
\cs_set:Nn \__kessels_list_map:n { #2 }
\clist_map_function:cN { l_kessels_#1_clist } \__kessels_list_map:n
}
\clist_new:N \l__kessels_list_temp_clist
\cs_new_protected:Nn \kessels_list_subtract:nn
{
% clear the temporary list
\clist_clear:N \l__kessels_list_temp_clist
% map the original list
\clist_map_inline:cn { l_kessels_#1_clist }
{
% if the item doesn't appear in the remove list, add it to the temp one
\clist_if_in:cnF { l_kessels_#2_clist } { ##1 }
{
\clist_put_right:Nn \l__kessels_list_temp_clist { ##1 }
}
}
% reconstitute the original list
\clist_set_eq:cN { l_kessels_#1_clist } \l__kessels_list_temp_clist
}
\ExplSyntaxOff
\begin{document}
\definelist{original}{Lima,Alpha,Delta,Oscar,Tango,Whisky,Echo,Romeo,Xray}
\definelist{remove}{Whisky,Oscar,Romeo,Delta}
\uselist{original}{, }
\copylist{original}{changed}
\subtractlist{changed}{remove}
\uselist{changed}{, }
\maplist{changed}{\fbox{#1\vphantom{Ay}} }
\maplist{original}{\fbox{#1\vphantom{Ay}} }
\end{document}
答案3
另一种方法listofitems
。本质上,我使用 remove-list 作为列表分隔符。因此,它们不会出现在摘要列表反刍中,因为它们作为列表分隔符被删除(连同多余的逗号)。
\documentclass{article}
\usepackage{listofitems}
\newcommand\subtractlists[2]{%
\expandafter\setsepchar\expandafter{\expandafter,\expandafter/#2}%
\readlist*\mylist{#1}%
\foreachitem\z\in\mylist[]{%
\ifnum\zcnt>1\relax\ifnum\listlen\mylist[\zcnt]=1\relax,\fi\fi
\foreachitem\zz\in\mylist[\zcnt]{%
\ifnum\zzcnt>0\relax\zz\fi}}%
}
\begin{document}
\def\originallist{Lima,Alpha,Delta,Oscar,Tango,Whisky,Echo,Romeo,Xray}
List: \originallist
\def\removelist{Whisky||Oscar||Romeo||Delta}
Remove: \removelist
Result:
\subtractlists{\originallist}{\removelist}
\end{document}
如果坚持删除列表用逗号分隔(而不是||
),则需要做一些额外的事情:
\documentclass{article}
\usepackage{listofitems}
\makeatletter
\newcommand\subtractlists[2]{%
\setsepchar{,}%
\readlist\remlist{#2}%
\def\tmp{,/}%
\foreachitem\z\in\remlist[]{\ifnum\zcnt=1\else\g@addto@macro\tmp{||}\fi
\expandafter\g@addto@macro\expandafter\tmp\expandafter{\z}}%
\expandafter\setsepchar\expandafter{\tmp}%
\readlist*\mylist{#1}%
\foreachitem\z\in\mylist[]{%
\ifnum\zcnt>1\relax\ifnum\listlen\mylist[\zcnt]=1\relax,\fi\fi
\foreachitem\zz\in\mylist[\zcnt]{%
\ifnum\zzcnt>0\relax\zz\fi}}%
}
\makeatother
\begin{document}
\def\originallist{Lima,Alpha,Delta,Oscar,Tango,Whisky,Echo,Romeo,Xray}
List: \originallist
\def\removelist{Whisky,Oscar,Romeo,Delta}
Remove: \removelist
Result:
\subtractlists{\originallist}{\removelist}
\end{document}
答案4
etoolbox
还提供列表处理功能。下面我实现了一些与您的需求类似的功能:
\documentclass{article}
\usepackage{etoolbox}
\newcommand{\createlist}[1]{\forcsvlist{\listadd{#1}}}% Create a list
\newcommand{\subtractlists}[3][]{% Subtract two lists
% Check for empty optional argument (https://tex.stackexchange.com/a/53091/5764)
\if\relax\detokenize{#1}\relax
% How each item is processed
\renewcommand*{\do}[1]{\listremove{#2}{##1}}% Remove elements from original list
\else
\let#1#2% Copy original list
% How each item is processed
\renewcommand*{\do}[1]{\listremove{#1}{##1}}% Remove elements from optional list argument
\fi
\dolistloop{#3}% Process list
}
% https://tex.stackexchange.com/a/89187/5764
\newcommand{\printlist}[2][,\,]{% \printlist[<delim>]{<list cs>}
\def\itemdelim{\def\itemdelim{#1}}% Item delimiter delayed by one cycle
\renewcommand*{\do}[1]{\itemdelim##1}% How each item is processed
\dolistloop{#2}}% Process list
\setlength{\parindent}{0pt}% Just for this example
\begin{document}
% Create some lists for testing
\createlist\originallist{Lima,Alpha,Delta,Oscar,Tango,Whisky,Echo,Romeo,Xray}
\createlist\removelist{Whisky,Oscar,Romeo,Delta}
\verb|\originallist|: \printlist\originallist \par
\verb|\removelist|: \printlist\removelist \par
\verb|\subtractlists{\originallist}{\removelist}|:
\subtractlists[\processedlist]{\originallist}{\removelist}\printlist\processedlist \par
\verb|\processedlist|: \printlist\processedlist
\end{document}