这两天在写一个文档的模板,其中需要用到逗号分隔列表来遍历处理分隔的各项条目。最开始的时候把问题考虑复杂了,最后查阅了 LaTeX3 的手册——Source3.pdf
,使用 LaTeX3 方法解决了问题。
问题描述
以一个简单的例子来说,现有一列表示桥型的名称如:简支梁桥
、连续梁桥
、拱桥
、桁架桥
、斜拉桥
、悬索桥
……同时还有这些桥型对应的桥梁数量,存储在名为 <桥型>
+数量
的宏命令中,如 \简支梁桥数量
、\连续梁桥数量
、\拱桥数量
、\桁架桥数量
、\斜拉桥数量
、\悬索桥数量
…… (不用惊奇,xelatex
作为编译引擎时可以使用 CJK 字符作为宏命令字符。)现在的问题是:如何设计一个宏命令 \桥型调查{<调查列表>}
在其展开后能够形成:
调查的桥型包括:简支梁桥、连续梁桥、……、斜拉桥以及悬索桥,其中:简支梁桥的数量为 xx 座,连续梁桥的数量为 yy 座,……,悬索桥的数量为 xx 座。
这样的段落结构,其中,罗列的桥型则由这个宏命令的参数 <调查列表>
进行控制。
解决办法
给定条件
先把给定的条件用 LaTeX
语句写出来:
1 | \def\简支梁桥数量{7} |
分析解决简单的问题
在这个问题中,显然描述一种桥型数量的语句是最为容易定义的,不妨定义一个带单参数的宏命令:
1 | \newcommand\CountBridge[1]{#1的数量为\@nameuse{#1数量}座} |
当然,这一个命令的定义使用了内部命令 \@nameuse
,所以定义的语句一定要放在 \makeatletter
和 \makeatother
之间。这时,可以将 \CountBridge
这条语句在 ctexart
等支持中文的文档类中进行一个试验,就能得到:
悬索桥的数量是 3 座
如果需要进行遍历执行 \CountBridge
,一般首先想到的就是 pgffor
宏包中的 \foreach
(反正我不是很推荐使用内部命令 \@for \xx:=\set \do {...}
这种方式)。可以利用 xparse
包提供的定义命令的方法:
1 | \NewDocumentCommand\ListCountI{m}{ |
或者使用 xparse
包中提供的 \SplitList
和 \ProcessList
定义:
1 | \NewDocumentCommand\ListCountII{>{\SplitList{,}}m}{ |
结果显示的都是:
简支梁桥的数量为 7 座悬索桥的数量为 3 座斜拉桥的数量为 6 座
而语句:
1 | \foreach \x in {简支梁桥,悬索桥,斜拉桥} {\x} |
显示的则是:
简支梁桥悬索桥斜拉桥
显然,现在遇到的问题是条目项之间的分隔问题。
条目间的分隔问题
很显然,直接在遍历循环体内添加分隔标点符号是不合适的,无论是修改 \foreach
的遍历循环体:
1 | \foreach \x in {简支梁桥,悬索桥,斜拉桥} {\CountBridge{\x},} |
抑或是修改 \CountBridge
的定义:
1 | \newcommand\CountBridge[1]{#1的数量为\@nameuse{#1数量}座,} |
都会多出来一个不必要的分隔符号,因此需要使用一个判断语句来选择是否输出这个分隔符号。这两种解决方案中,前一条语句需要判断当前处理项是否为最末项,后一条语句则需要判断当前处理项是否为首项。显然,判断首项比判断末项容易得多,接下来就以判断首项决定输出的思路进行设计,这里有相对优雅和相对不优雅的两种做法。
不优雅的做法
不优雅的做法适用于修改 \CountBridge
的定义的方法,这时要直接粗暴地创建一个 bool
变量,在 \CountBridge
中进行判断分支,做法如下:
1 | \newif\iffirstitem\firstitemtrue |
这种做法不优雅之处在于,经过执行 \CountBridge
后,\iffirstitem
的值一定为 false
,必须通过 \firstitemtrue
来把其值改为 true
,否则,当执行另一个列表时,首项前一定多一个分隔标点符号。\firstitemtrue
通常应在遍历处理后立即执行,因此,\ListCountII
这个命令应被改为:
1 | \NewDocumentCommand\ListCountII{>{\SplitList{,}}m}{ |
这里,我把遍历后末尾的句号也加上了。
优雅的做法
优雅的做法适合于修改 \foreach
的遍历循环体,利用 \foreach
循环变量的可选选项 [count = \i]
,结合 ifthen
宏包的判断分支功能,命令\ListCountI
应该被修改为:
1 | \NewDocumentCommand\ListCountI{m}{ |
同样的,遍历后末尾的句号也添加上了。
段落前面的概括
至此,目标段落“其中,……”这一部分已经定义完成,目标段落前面还有一句话要概括统计的桥型,显然也可以用前面两种方法。不过,按人类语言通常习惯,在表述两个条目时我们习惯用“和”作为连词,在三个及以上条目时,我们习惯先用“、”分隔,到最后一个条目时用“和”作为连词。这个比较特殊的结构一开始我首先想到的是 siunitx
宏包中对数字列表的处理方法,于是去翻起了 siunitx.sty
的代码,这个包的代码长度甚至超过了 LaTeXe
的内核…… 看着一个一个嵌套的函数,对数字、单位的各种判断处理,我有些绝望了,于是翻起 LaTeX3 的手册——Source3.pdf
,在其中发现了逗号分隔列表的处理函数,居然有非常简便的方法,那就是直接使用 \clist_use:Nnnn
解决问题,嗯,看来 LaTeX3 真需要系统地学一下了。解决方案的核心语句是:
1 | \clist_set:Nn \l_my_clist {简支梁桥,悬索桥,斜拉桥} |
当然,因为使用了 LaTeX3 语法,需要 \ExplSyntaxOn
和 \ExplSyntaxOff
来开启关闭环境,从而能正确识别相应的命令。
最终解决方案,完整的一个 MWE 如下:
1 | \documentclass{ctexart} |
排版出来的内容为:
调查的桥型包括:简支梁桥、悬索桥、斜拉桥、拱桥以及桁架桥,其中:简支梁桥的数量为 7 座,悬索桥的数量为 3 座,斜拉桥的数量为 6 座,拱桥的数量为 5 座,桁架桥的数量为 4 座。
完美解决问题。