LaTeX 里逗号分隔列表的处理

这两天在写一个文档的模板,其中需要用到逗号分隔列表来遍历处理分隔的各项条目。最开始的时候把问题考虑复杂了,最后查阅了 LaTeX3 的手册——Source3.pdf,使用 LaTeX3 方法解决了问题。

问题描述

以一个简单的例子来说,现有一列表示桥型的名称如:简支梁桥连续梁桥拱桥桁架桥斜拉桥悬索桥……同时还有这些桥型对应的桥梁数量,存储在名为 <桥型>+数量 的宏命令中,如 \简支梁桥数量\连续梁桥数量\拱桥数量\桁架桥数量\斜拉桥数量\悬索桥数量…… (不用惊奇,xelatex 作为编译引擎时可以使用 CJK 字符作为宏命令字符。)现在的问题是:如何设计一个宏命令 \桥型调查{<调查列表>} 在其展开后能够形成:

调查的桥型包括:简支梁桥、连续梁桥、……、斜拉桥以及悬索桥,其中:简支梁桥的数量为 xx 座,连续梁桥的数量为 yy 座,……,悬索桥的数量为 xx 座。

这样的段落结构,其中,罗列的桥型则由这个宏命令的参数 <调查列表> 进行控制。

解决办法

给定条件

先把给定的条件用 LaTeX 语句写出来:

1
2
3
4
5
6
\def\简支梁桥数量{7}
\def\连续梁桥数量{8}
\def\拱桥数量{5}
\def\桁架桥数量{4}
\def\斜拉桥数量{6}
\def\悬索桥数量{3}

分析解决简单的问题

在这个问题中,显然描述一种桥型数量的语句是最为容易定义的,不妨定义一个带单参数的宏命令:

1
\newcommand\CountBridge[1]{#1的数量为\@nameuse{#1数量}座}

当然,这一个命令的定义使用了内部命令 \@nameuse,所以定义的语句一定要放在 \makeatletter\makeatother 之间。这时,可以将 \CountBridge 这条语句在 ctexart 等支持中文的文档类中进行一个试验,就能得到:

悬索桥的数量是 3 座

如果需要进行遍历执行 \CountBridge,一般首先想到的就是 pgffor 宏包中的 \foreach(反正我不是很推荐使用内部命令 \@for \xx:=\set \do {...} 这种方式)。可以利用 xparse 包提供的定义命令的方法:

1
2
3
\NewDocumentCommand\ListCountI{m}{
\foreach \x in {简支梁桥,悬索桥,斜拉桥} {\CountBridge{\x}}
}

或者使用 xparse 包中提供的 \SplitList\ProcessList 定义:

1
2
3
\NewDocumentCommand\ListCountII{>{\SplitList{,}}m}{
\ProcessList{#1}\CountBridge
}

结果显示的都是:

简支梁桥的数量为 7 座悬索桥的数量为 3 座斜拉桥的数量为 6 座

而语句:

1
\foreach \x in {简支梁桥,悬索桥,斜拉桥} {\x}

显示的则是:

简支梁桥悬索桥斜拉桥

显然,现在遇到的问题是条目项之间的分隔问题。

条目间的分隔问题

很显然,直接在遍历循环体内添加分隔标点符号是不合适的,无论是修改 \foreach 的遍历循环体:

1
2
\foreach \x in {简支梁桥,悬索桥,斜拉桥} {\CountBridge{\x},}
\foreach \x in {简支梁桥,悬索桥,斜拉桥} {,\CountBridge{\x}}

抑或是修改 \CountBridge 的定义:

1
2
\newcommand\CountBridge[1]{#1的数量为\@nameuse{#1数量}座,}
\newcommand\CountBridge[1]{,#1的数量为\@nameuse{#1数量}座}

都会多出来一个不必要的分隔符号,因此需要使用一个判断语句来选择是否输出这个分隔符号。这两种解决方案中,前一条语句需要判断当前处理项是否为最末项,后一条语句则需要判断当前处理项是否为首项。显然,判断首项比判断末项容易得多,接下来就以判断首项决定输出的思路进行设计,这里有相对优雅和相对不优雅的两种做法。

不优雅的做法

不优雅的做法适用于修改 \CountBridge 的定义的方法,这时要直接粗暴地创建一个 bool 变量,在 \CountBridge 中进行判断分支,做法如下:

1
2
3
\newif\iffirstitem\firstitemtrue
\newcommand\CountBridge[1]{%
\iffirstitem\firstitemfalse\else\fi#1的数量为\@nameuse{#1数量}座}

这种做法不优雅之处在于,经过执行 \CountBridge 后,\iffirstitem 的值一定为 false,必须通过 \firstitemtrue 来把其值改为 true,否则,当执行另一个列表时,首项前一定多一个分隔标点符号。\firstitemtrue 通常应在遍历处理后立即执行,因此,\ListCountII 这个命令应被改为:

1
2
3
\NewDocumentCommand\ListCountII{>{\SplitList{,}}m}{
\ProcessList{#1}\CountBridge\firstitemtrue
}

这里,我把遍历后末尾的句号也加上了。

优雅的做法

优雅的做法适合于修改 \foreach 的遍历循环体,利用 \foreach 循环变量的可选选项 [count = \i],结合 ifthen 宏包的判断分支功能,命令\ListCountI 应该被修改为:

1
2
3
4
5
\NewDocumentCommand\ListCountI{m}{
\foreach \x[count=\i] in {简支梁桥,悬索桥,斜拉桥} {
\ifthenelse{\equal{\i}{1}}{\relax}{,}%
\CountBridge{\x}}。
}

同样的,遍历后末尾的句号也添加上了。

段落前面的概括

至此,目标段落“其中,……”这一部分已经定义完成,目标段落前面还有一句话要概括统计的桥型,显然也可以用前面两种方法。不过,按人类语言通常习惯,在表述两个条目时我们习惯用“和”作为连词,在三个及以上条目时,我们习惯先用“、”分隔,到最后一个条目时用“和”作为连词。这个比较特殊的结构一开始我首先想到的是 siunitx 宏包中对数字列表的处理方法,于是去翻起了 siunitx.sty 的代码,这个包的代码长度甚至超过了 LaTeXe 的内核…… 看着一个一个嵌套的函数,对数字、单位的各种判断处理,我有些绝望了,于是翻起 LaTeX3 的手册——Source3.pdf,在其中发现了逗号分隔列表的处理函数,居然有非常简便的方法,那就是直接使用 \clist_use:Nnnn 解决问题,嗯,看来 LaTeX3 真需要系统地学一下了。解决方案的核心语句是:

1
2
\clist_set:Nn \l_my_clist {简支梁桥,悬索桥,斜拉桥}
\clist_use:Nnnn \l_my_clist { 和 } { 、 } { 和 }

当然,因为使用了 LaTeX3 语法,需要 \ExplSyntaxOn\ExplSyntaxOff 来开启关闭环境,从而能正确识别相应的命令。

最终解决方案,完整的一个 MWE 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
\documentclass{ctexart}
\usepackage{pgffor,ifthen} % xparse 由 ctex 宏包自动载入了,不需要加载
\def\简支梁桥数量{7}
\def\连续梁桥数量{8}
\def\拱桥数量{5}
\def\桁架桥数量{4}
\def\斜拉桥数量{6}
\def\悬索桥数量{3}
\makeatletter
\newcommand\CountBridge[1]{#1的数量为\@nameuse{#1数量}座}
\makeatother
\ExplSyntaxOn
\NewDocumentCommand\桥型调查{m}{
\clist_set:Nn \l_my_clist {#1}
调查的桥型包括:\clist_use:Nnnn \l_my_clist { 和 } { 、 } { 以及 },
其中:\foreach \x[count =\i] in {#1} {\ifthenelse{\equal{\i}{1}}{\relax}{,}\CountBridge{\x}}。
}
\ExplSyntaxOff

\begin{document}
\桥型调查{简支梁桥,悬索桥,斜拉桥,拱桥,桁架桥}
\end{document}

排版出来的内容为:

调查的桥型包括:简支梁桥、悬索桥、斜拉桥、拱桥以及桁架桥,其中:简支梁桥的数量为 7 座,悬索桥的数量为 3 座,斜拉桥的数量为 6 座,拱桥的数量为 5 座,桁架桥的数量为 4 座。

完美解决问题。

感谢拨冗阅读本文,若有些许收获,不妨捐赠以资鼓励。