0%

本文介绍如何在 LaTeX2e 中实现答案控制的效果,也即是答案统一在后面显示,考虑到有些朋友没有学习 LaTeX3,所以本文尽量用 LaTeX2e & TeX 来实现,如果对 LaTeX3 感兴趣,可以参考下面的文章

使用 LaTeX3 在试卷中做一个答案统计

用l3的seq实现习题答案统一输出

实现原理

我们的想法其实就是要在前面输入答案,但是不做输出,而是存起来,然后放到指定位置输出,例如

1
2
3
4
5
6
1.这是题目这是题目...
\answer{C}
2.这是题目这是题目...
\answer{B}
...
\showanswer

大概就是这样,使用 \showanswer 命令来显示答案

讨论

不知道大家有没有注意过一个问题,在标准文档类中的 \author,\title,\date , 这些命令使用后并不会立即输出,而是等到 \maketitle 命令的到来才会输出,其实它就在做这样一个事情

为了更加清楚,我们来看看其源码,在命令行使用 latexdef -s \author 即可查看其定义

\author

哎,简化一下吧,大概类似于

1
\newcommand*{\author}[1]{\newcommand{\@author}{#1}}

嘿,我们再来看看 \maketitle 命令呢,同样使用 latexdef -c article \maketitle

\maketitle

好吧,有点乱,我也没读懂,我猜测应该是设置了一些格式,然后将前面定义的所有与封面相关的元素全部删除了,好像没有多少对我们有用的信息呢. 咿,我发现有个 \@maketitle ,这小家伙藏得还挺深,还是被我揪出来了latexdef -c article \@maketitle

\@maketitle

原来都藏在这儿呢,在封面使用前面定义的 \@author \@title 等,注意了,不是 \author \title 哦.

(做封面的朋友该有想法了)

回来吧,现在我们应该清楚了,\author 命令的作用只是在间接定义 \@author ,实际并不做输出

开始动手

设计输入

我们也可以仿照它的做法,来实现我们的效果,使用如下代码

1
\newcommand{\answer}[1]{\def\@answer{#1}}

这里使用 \def 是为了避免 Command \@answer already defined. 的问题

但是这样有一个问题,当我们使用

1
2
3
\answer{A}
\answer{B}
\answer{C}

后,实际保存在 \@answer 里面的值是 C,所以我们只用一个宏来保存答案似乎不太好办(LaTeX3 里面倒是很简单),最好是为每一个题目都分配一个宏来保存答案,考虑如下

1
2
3
4
\newcommand{\answerI}[1]{\def\@answerI{#1}}
\newcommand{\answerII}[1]{\def\@answerII{#1}}
\newcommand{\answerIII}[1]{\def\@answerIII{#1}}
...

一张试卷 20 多个题目,难道我们要定义 20 多个命令?太麻烦了!如果能自动根据题号来定义命令就好了

唉,我想到了计数器,为其分配一个计数器 answer,尝试如下代码

1
\newcommand{\answer}[1]{\def\answer\theanswer{#1}}

这样,使用第一次 \answer 就定义了 \answer1 , 第二次就定义了 \answer2 ,但是这样定义有两个问题

  1. \def\answer\theanswer 本身是有问题的
  2. \answer1 这样的命令命名是有问题的,数字正常情况下不能作为命令名

但是,不要气馁,我们正在慢慢接近答案

解决第一个问题就涉及到 TeX 中 迷人的宏展开 了,当然这里只涉及到很简单的

介绍两个命令 \csname\endcsname , 我们使用如下命令

1
2
3
\csname textbf \endcsname{abc} % ---> \textbf{abc}
\def\aa{textbf}
\csname \aa \endcsname{abc} % ---> \textbf{abc}

也就是说,夹在 \csname\endcsname 之间的部分会先被完全展开至不可展开为止,然后再为其加上 \ 使其成为一个控制序列(命令)

解决第二个问题也很简单,我们只需将数字转化为罗马数字即可,这里是计数器,我们直接使用现成的\roman{answer} 即可

我们考虑如下定义

1
2
3
4
5
\def\answer#1
{
\expandafter\gdef\csname answer\roman{answer}\endcsname{#1}
\stepcounter{answer}
}

这里使用了 \expandafter ,是为了跳过 \def 先展开 \csname\endcsname 之间的内容,然后再 \def

设计输出

输出的话我们可以

1
2
3
4
\answeri
\answerii
\answeriii
...

但更好的是使用循环来做这件事情,这里使用 pgffor 宏包提供的 \foreach 命令

1
2
3
4
\foreach[parse] \x in{1,2,...,\theanswer}
{
\csname answer\romannumeral\x \endcsname
}

注意这里的 \romannumeral 接受一个数字, 并将其转化为小写罗马数字,区别于刚刚的 \roman , 它是接受一个计数器,而不是接受数字

实现源码

现在我们将代码整合一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
\documentclass{ctexart}
\usepackage{pgffor}
\newcounter{answer}
\setcounter{answer}{1}
\def\answer#1
{
\expandafter\gdef\csname answer\roman{answer}\endcsname{#1}
\stepcounter{answer}
}
\def\showanswer
{
\foreach[parse] \x in{1,2,...,\theanswer}
{
\csname answer\romannumeral\x \endcsname
}
}
\begin{document}
\begin{enumerate}
\item 这是题目这是题目 \answer{A}
\item 这是题目这是题目 \answer{D}
\item 这是题目这是题目 \answer{B}
\item 这是题目这是题目 \answer{C}
\item 这是题目这是题目 \answer{B}
\end{enumerate}
答案分别是:\showanswer
\end{document}

输出

输出

好了,至此本文结束,下期再见吧 ~

问题的提出

在群里有老师想要实现如下的一个答案统计效果

这本来是 exam 文档类提供的一个功能,奈何有些老师不想使用这个文类,我们只好造一个轮子

分析问题

我们需要注意到:

  • 这个答案列表应该是在排版选择题的时候就输入,而不是最后来输入
  • 也就是我们需要先将前面的答案暂存起来,最后再来排版出来
  • 计数功能,我们还需要去检测答案列表中各个选项的数量

解决问题

首先我们需要一个排版选择题答案的命令

有不少老师在使用 \xx{}{}{}{}这个命令,如果你没有类似的命令,我仿造他们的思路造了一个这样的命令\choices{}{}{}{},源码如下,这里参考了公众号不会功夫的熊猫的方法

这里排版选择题我特别推荐xkwxdyyxchoices 宏包,这个宏包功能十分强大,对选择题可以很灵活的排版,项目地址 https://github.com/xkwxdyy/xchoices

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
\ExplSyntaxOn
\cs_new_protected:Npn \g_choices: #1#2#3#4 {
\par\noindent
\box_clear_new:N \l_choiceA_box
\box_clear_new:N \l_choiceB_box
\box_clear_new:N \l_choiceC_box
\box_clear_new:N \l_choiceD_box

\hbox_set:Nn \l_choiceA_box{A.#1~~}
\hbox_set:Nn \l_choiceB_box{B.#2~~}
\hbox_set:Nn \l_choiceC_box{C.#3~~}
\hbox_set:Nn \l_choiceD_box{D.#4~~}

\dim_zero_new:N \l_choice_wd_max_AB
\dim_zero_new:N \l_choice_wd_max_CD
\dim_zero_new:N \l_choice_wd_max
\dim_set:Nn \l_choice_wd_max_AB {\dim_max:nn{\box_wd:N \l_choiceA_box}{\box_wd:N \l_choiceB_box}}
\dim_set:Nn \l_choice_wd_max_CD {\dim_max:nn{\box_wd:N \l_choiceC_box}{\box_wd:N \l_choiceD_box}}
\dim_set:Nn \l_choice_wd_max {\dim_max:nn{\dim_use:N \l_choice_wd_max_AB}{\dim_use:N \l_choice_wd_max_CD}}

\dim_compare:nNnTF{\dim_use:N \l_choice_wd_max} < {0.25\linewidth}
{
\box_set_wd:Nn \l_choiceA_box {0.25\linewidth}
\box_set_wd:Nn \l_choiceB_box {0.25\linewidth}
\box_set_wd:Nn \l_choiceC_box {0.25\linewidth}
\box_set_wd:Nn \l_choiceD_box {0.25\linewidth}

\box_use:N \l_choiceA_box
\box_use:N \l_choiceB_box
\box_use:N \l_choiceC_box
\box_use:N \l_choiceD_box
}
{
\dim_compare:nNnTF{\dim_use:N \l_choice_wd_max} < {0.5\linewidth}
{
\box_set_wd:Nn \l_choiceA_box {0.45\linewidth}
\box_set_wd:Nn \l_choiceB_box {0.45\linewidth}
\box_set_wd:Nn \l_choiceC_box {0.45\linewidth}
\box_set_wd:Nn \l_choiceD_box {0.45\linewidth}

\box_use:N \l_choiceA_box
\box_use:N \l_choiceB_box
\par\vspace*{0.5em}\hspace{2em}
\box_use:N \l_choiceC_box
\box_use:N \l_choiceD_box
}
{
\box_use:N \l_choiceA_box
\par\vspace*{0.5em}\hspace{2em}
\box_use:N \l_choiceB_box
\par\vspace*{0.5em}\hspace{2em}
\box_use:N \l_choiceC_box
\par\vspace*{0.5em}\hspace{2em}
\box_use:N \l_choiceD_box
}
}
}
\cs_set_eq:NN \choices \g_choices:
\ExplSyntaxOff

有了个命令,就比较方便了,我们只需要在原有的命令上面加上一个参数来接受正确答案的选项就好了

1
2
3
4
5
6
7
\NewDocumentCommand{\choices}{ommmm}
{
\IfNoValueTF{#1}
{\seq_put_right:Nn\__answer_list_seq{\space}}
{\seq_put_right:Nn\__answer_list_seq{#1}}
\choice{#2}{#3}{#4}{#5}
}

这里重新定义了一个\choices[]{}{}{}{} 命令,第一个可选参数接受正确答案

观察到上面有三行代码

1
2
3
\IfNoValueTF{#1}
{\seq_put_right:Nn\__answer_list_seq{\space}}
{\seq_put_right:Nn\__answer_list_seq{#1}}

这行代码是用于检测第一个可选参数,如果有可选参数,则将接受到的参数向右追加到 \__answer_list_seq 这个序列里面,如果没有可选参数,追加一个 \space 标记

下面我们需要定义和初始化一些变量

1
2
\seq_clear_new:N \__answer_list_seq % 用于保存答案列表
\int_zero_new:N \__choice_amount_int % 用于获取各个选项数量

我们来解释一些上面的 \seq_put_right:Nn 函数,例如现在我们有一个空序列变量 \__answer_list_seq 我们使用如下命令后

1
2
3
4
5
\choices[A]{A}{B}{C}{D}
\choices[B]{A}{B}{C}{D}
\choices[C]{A}{B}{C}{D}
\choices[A]{A}{B}{C}{D}
\choices[C]{A}{B}{C}{D}

此时这个\__answer_list_seq 的值就是 ABCAC

使用如下命令可以获取到 \__answer_list_seq 这个序列某项的值

1
\seq_item:Nn\__answer_list_seq{2} % ---> B

那么我们来定义一个命令来显示答案列表,\seq_count:N 命令用于获取序列的长度,\int_step_inline:nnn 用于创建一个整数循环

1
2
3
4
5
\NewDocumentCommand{\answerlists}{}
{
答案列表:~\int_step_inline:nnn{1}{\seq_count:N \__answer_list_seq}
{##1.\seq_item:Nn\__answer_list_seq{##1}~}
}

此时再使用 \answerlists 命令就可以得到

答案列表: 1.A 2.B 3.C 4.A 5.C

下面的问题就是来解决这个选项计数的功能

例如现在我们的 \__answer_list_seq 中保存的值为 ABCAC ,我们如何去获取各个选项的值呢?

当然这里有一个思路,可以用 \seq_map_inline:Nn ,然后使用一个加法计数器来得到各选项的值,但是感觉比较麻烦,所以这里我将使用正则表达式来获取匹配到的数量

1
\regex_count:nnN{A}{ABCDAA}\l_tmpa_int % ---> \l_tmpa_int = 3

这个 \regex_count:nnN 函数接受三个参数,第一个接受一个正则表达式,第二个接受待匹配内容,第三个接受一个整数变量,用于保存匹配到的次数的整数值

我们尝试

1
regex_count:nnN{A}{\__answer_list_seq}\__choice_amount_int

这样是不行的,因为第二个参数的 n 变体,无法为我们展开 \__answer_list_seq ,我们需要定义一个 V 变体

1
\cs_generate_variant:Nn\regex_count:nnN{nVN}

这样我们就可以使用

1
regex_count:nVN{A} \__answer_list_seq \__choice_amount_int

来得到选项 A 的数量了

现在来定义一个选项计数的命令

1
2
3
4
5
6
7
8
9
10
11
\NewDocumentCommand{\choicescount}{}
{
选项计数: ~A:\regex_count:nVN{A}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad B:\regex_count:nVN{B}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad C:\regex_count:nVN{C}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad D:\regex_count:nVN{D}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
}

至此,这个问题已经被解决了

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
\documentclass{ctexart}
\usepackage{geometry}
\geometry{margin=2cm}
\begin{document}
%%%%%%%%%%%%%%%%%% 定义\choice{}{}{}{}命令,如果你有,可以注释它 %%%%%%%%%%%%%%%%%%%%%
\input{choice.tex} %% 这部分代码在上文中
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\ExplSyntaxOn
\seq_clear_new:N \__answer_list_seq
\int_zero_new:N \__choice_amount_int
\cs_generate_variant:Nn\regex_count:nnN{nVN}
\NewDocumentCommand{\choice}{ommmm}
{
\IfNoValueTF{#1}
{\seq_put_right:Nn\__answer_list_seq{\space
{\seq_put_right:Nn\__answer_list_seq{#1}}
\choice{#2}{#3}{#4}{#5}
}
\NewDocumentCommand{\answerlists}{}
{
答案列表:~\int_step_inline:nnn{1}{\seq_count:N \__answer_list_seq}
{##1.\seq_item:Nn\__answer_list_seq{##1}~}
}
\NewDocumentCommand{\choicescount}{}
{
选项计数: ~A:\regex_count:nVN{A}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad B:\regex_count:nVN{B}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad C:\regex_count:nVN{C}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad D:\regex_count:nVN{D}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
}
\choice[A]{A}{B}{C}{D}
\choice[B]{A}{B}{C}{D}
\choice[C]{A}{B}{C}{D}
\choice[A]{A}{B}{C}{D}
\choice[C]{A}{B}{C}{D}
\choice[D]{A}{B}{C}{D}
\choice[B]{A}{B}{C}{D}
\choice[A]{A}{B}{C}{D}
\choice[D]{A}{B}{C}{D}
\choice[B]{A}{B}{C}{D}
\par
\vspace*{2cm}
\answerlists
\par
\choicescount
\ExplSyntaxOff
\end{document}

输出结果

样式加强

我们还可以修改一下输出样式

效果图如下

我们修改一下 \answerlists 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
\NewDocumentCommand{\answerlists}{s}
{
\IfBooleanTF{#1}
{
\int_step_inline:nn{\seq_count:N \__answer_list_seq}
{\seq_put_right:Nn \__answer_amount_seq{##1}}
\begin{tabularx}{\seq_count:N \__answer_list_seq cm}
{|*{\seq_count:N \__answer_list_seq}{>{\centering\arraybackslash}X|}}
\hline
\seq_use:Nn \__answer_amount_seq{&}\\ \hline
\seq_use:Nn \__answer_list_seq{&}\\ \hline
\end{tabularx}
}
{
答案列表:~\int_step_inline:nnn{1}{\seq_count:N \__answer_list_seq}
{##1.\seq_item:Nn\__answer_list_seq{##1}~}
}
}

可以发现增加了

1
2
3
4
5
6
7
8
\int_step_inline:nn{\seq_count:N \__answer_list_seq}
{\seq_put_right:Nn \__answer_amount_seq{##1}}
\begin{tabularx}{\seq_count:N \__answer_list_seq cm}
{|*{\seq_count:N \__answer_list_seq}{>{\centering\arraybackslash}X|}}
\hline
\seq_use:Nn \__answer_amount_seq{&}\\ \hline
\seq_use:Nn \__answer_list_seq{&} \\ \hline
\end{tabularx}

观察前两行代码,这里是通过 \int_step_inline:nn 构建了一个序列,这个序列\__answer_amount_seq 的值为1234 ... \seq_count:N \__answer_list_seq ,随后使用 tabularx 环境来排版这个表格,同时需要 \usepackage{tabularx} 宏包的支持

然后就是 \seq_use:Nn 这个函数,是用来将序列的值通过某个 token 连接起来,例如,此时 \__answer_list_seq 的值为 ABCDAC ,那么使用 \seq_use:Nn \__answer_list_seq{&} 后,将被扩展为

A & B & C & D & A & C

同时,我们也修改 \choicescount 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
\NewDocumentCommand{\choicescount}{s}
{
\IfBooleanTF{#1}
{
\begin{tabular}{|c|c|c|c|c|}
\hline
选项 & A & B & C & D \\\hline
计数
& \regex_count:nVN{A}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
& \regex_count:nVN{B}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
& \regex_count:nVN{C}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
& \regex_count:nVN{D}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int \\ \hline
\end{tabular}
}
{
选项计数: ~A:\regex_count:nVN{A}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad B:\regex_count:nVN{B}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad C:\regex_count:nVN{C}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad D:\regex_count:nVN{D}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
}
}

关于 \NewDocumentCommand 中的参数规范 s 可自行查阅 xparse 宏包用户说明文档

通过前面的工作我们构建了四个命令,分别是

  • \answerlists

  • \answerlists*

  • \choicescount

  • \choicescount*

带星号的命令用于输出表格形式

加强完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
\documentclass{ctexart}
\usepackage{geometry,tabularx}
\geometry{margin=2cm}
\begin{document}
\input{choice.tex}
\ExplSyntaxOn
\seq_clear_new:N \__answer_list_seq
\seq_clear_new:N \__answer_amount_seq
\int_zero_new:N \__choice_amount_int
\cs_generate_variant:Nn\regex_count:nnN{nVN}

\NewDocumentCommand{\choices}{ommmm}
{
\IfNoValueTF{#1}{\seq_put_right:Nn\__answer_list_seq{\space}}{\seq_put_right:Nn\__answer_list_seq{#1}}
\choice{#2}{#3}{#4}{#5}
}
\NewDocumentCommand{\answerlist}{s}
{
\IfBooleanTF{#1}
{
\int_step_inline:nn{\seq_count:N \__answer_list_seq}
{\seq_put_right:Nn \__answer_amount_seq{##1}}
\begin{tabularx}{\seq_count:N \__answer_list_seq cm}
{|*{\seq_count:N \__answer_list_seq}{>{\centering\arraybackslash}X|}}
\hline
\seq_use:Nn \__answer_amount_seq{&}\\ \hline
\seq_use:Nn \__answer_list_seq{&}\\ \hline
\end{tabularx}
}
{
答案列表:~\int_step_inline:nnn{1}{\seq_count:N \__answer_list_seq}
{##1.\seq_item:Nn\__answer_list_seq{##1}~}
}
}
\NewDocumentCommand{\choicecount}{s}
{
\IfBooleanTF{#1}
{
\begin{tabular}{|c|c|c|c|c|}
\hline
选项 & A & B & C & D \\\hline
计数
& \regex_count:nVN{A}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
& \regex_count:nVN{B}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
& \regex_count:nVN{C}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
& \regex_count:nVN{D}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int \\ \hline
\end{tabular}
}
{
选项计数: ~A:\regex_count:nVN{A}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad B:\regex_count:nVN{B}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad C:\regex_count:nVN{C}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
\quad D:\regex_count:nVN{D}\__answer_list_seq\__choice_amount_int
\int_use:N \__choice_amount_int
}
}

\choices[A]{A}{B}{C}{D}
\choices[B]{A}{B}{C}{D}
\choices[C]{A}{B}{C}{D}
\choices[A]{A}{B}{C}{D}
\choices[C]{A}{B}{C}{D}
\choices[D]{A}{B}{C}{D}
\choices[B]{A}{B}{C}{D}
\choices[A]{A}{B}{C}{D}
\choices[D]{A}{B}{C}{D}
\choices[B]{A}{B}{C}{D}
\par
\vspace*{2cm}
\centering
\answerlist*
\par
\choicecount*
\ExplSyntaxOff

\end{document}

输出结果


好了,就到这儿叭,我们下期再见啦 ~

这是LaTeX3的使用样例


定义颜色

colors.tex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
\definecolor{1}{RGB}{255,255,255}
\definecolor{2}{RGB}{255,20,147}
\definecolor{3}{RGB}{0,255,255}
\definecolor{4}{RGB}{255,20,147}
\definecolor{5}{RGB}{225,0,0}
\definecolor{6}{RGB}{225,255,0}
\definecolor{7}{RGB}{47,79,79}
\definecolor{8}{RGB}{25,25,112}
\definecolor{9}{RGB}{0,200,0}
\definecolor{10}{RGB}{124,252,0}
\definecolor{11}{RGB}{139,69,19}
\definecolor{12}{RGB}{250,128,114}
\definecolor{13}{RGB}{255,69,0}
\definecolor{14}{RGB}{153,50,204}
\definecolor{15}{RGB}{139,121,94}
\definecolor{16}{RGB}{205,193,197}
\definecolor{17}{RGB}{205,16,118}
\definecolor{18}{RGB}{238,154,0}
\definecolor{19}{RGB}{205,133,63}
\definecolor{20}{RGB}{139,0,139}

主文件

main.tex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
\documentclass[margin=5cm]{standalone}
\usepackage{tikz}
\pagecolor{black}
\input{colors.tex}
\ExplSyntaxOn
\NewDocumentCommand{\helix}{ m m m}
{
\draw[line~width=2pt,#2] #1+(0 \c_colon_str #3)--+(60 \c_colon_str #3)--+(120 \c_colon_str #3)--+(180 \c_colon_str #3)--+(240 \c_colon_str #3)--+(300 \c_colon_str #3)--cycle;
\draw[line~width=2pt,#2] #1+(10 \c_colon_str #3)--+(70 \c_colon_str #3)--+(130 \c_colon_str #3)--+(190 \c_colon_str #3)--+(250 \c_colon_str #3)--+(310 \c_colon_str #3)--cycle;
\draw[line~width=2pt,#2] #1+(20 \c_colon_str #3)--+(80 \c_colon_str #3)--+(140 \c_colon_str #3)--+(200 \c_colon_str #3)--+(260 \c_colon_str #3)--+(320 \c_colon_str #3)--cycle;
\draw[line~width=2pt,#2] #1+(30 \c_colon_str #3)--+(90 \c_colon_str #3)--+(150 \c_colon_str #3)--+(210 \c_colon_str #3)--+(270 \c_colon_str #3)--+(330 \c_colon_str #3)--cycle;
}
\ExplSyntaxOff
\begin{document}
\ExplSyntaxOn
\begin{tikzpicture}[x=0.1cm,y=0.1cm]
\int_step_variable:nnnNn{0}{10}{1080} \i
{
\helix{(\i \c_colon_str 2+1*\i)}{\int_rand:nn{1}{20}}{0.15*\i}
}
\end{tikzpicture}
\ExplSyntaxOff
\end{document}

输出
Image
将函数解析式稍加修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
\documentclass[margin=10cm]{standalone}
\usepackage{tikz}
\pagecolor{black}
\input{colors.tex}
\ExplSyntaxOn
\NewDocumentCommand{\helix}{ m m m}
{
\draw[line~width=2pt,#2] #1+(0 \c_colon_str #3)--+(60 \c_colon_str #3)--+(120 \c_colon_str #3)--+(180 \c_colon_str #3)--+(240 \c_colon_str #3)--+(300 \c_colon_str #3)--cycle;
\draw[line~width=2pt,#2] #1+(10 \c_colon_str #3)--+(70 \c_colon_str #3)--+(130 \c_colon_str #3)--+(190 \c_colon_str #3)--+(250 \c_colon_str #3)--+(310 \c_colon_str #3)--cycle;
\draw[line~width=2pt,#2] #1+(20 \c_colon_str #3)--+(80 \c_colon_str #3)--+(140 \c_colon_str #3)--+(200 \c_colon_str #3)--+(260 \c_colon_str #3)--+(320 \c_colon_str #3)--cycle;
\draw[line~width=2pt,#2] #1+(30 \c_colon_str #3)--+(90 \c_colon_str #3)--+(150 \c_colon_str #3)--+(210 \c_colon_str #3)--+(270 \c_colon_str #3)--+(330 \c_colon_str #3)--cycle;
}
\ExplSyntaxOff
\begin{document}
\ExplSyntaxOn
\begin{tikzpicture}[x=20cm,y=20cm]
\int_step_variable:nnnNn{0}{10}{360} \i
{
\helix{(\i \c_colon_str {1-sin(\i)})}{\int_rand:nn{1}{20}}{0.15}
}
\end{tikzpicture}
\ExplSyntaxOff
\end{document}

得到
Image


点赞,关注都是我的动力鸭~~~

本文是 LaTeX3 的一个简单使用样例

根据 C 语言的冒泡排序,改写成 LaTeX3, 如下:

C (部分代码)

1
2
3
4
5
6
7
8
9
10
11
12
for(i=0;i<4;i++)
{
for(j=0;j<4-i;j++)
{
if(a[j]>a[j+1])
{
t=a[j];
a[j]=a[j+1];
a[j+1]=t;
}
}
}

LaTeX3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
\documentclass[UTF8]{ctexart}
\ExplSyntaxOn
\int_new:N \l_t_int
\NewDocumentCommand{\sort}{m}
{

\intarray_const_from_clist:Nn \l_array_int {#1}
\int_step_variable:nNn{\intarray_count:N \l_array_int} \i
{
\int_step_variable:nNn{\intarray_count:N \l_array_int-\i} \j
{
\int_compare:nT {\intarray_item:Nn \l_array_int{\j} > \intarray_item:Nn\l_array_int{\j+1}}
{
\int_set_eq:NN \l_t_int \intarray_item:Nn \l_array_int{\j}
\intarray_gset:Nnn \l_array_int {\j}{\intarray_item:Nn \l_array_int{\j+1}}
\intarray_gset:Nnn \l_array_int {\j+1}{\l_t_int}
}
}
}
这些整数的由小到大的排序为:\int_step_inline:nn{\intarray_count:N \l_array_int}{\intarray_item:Nn\l_array_int {##1}~}
\cs_undefine:N \l_array_int
}
\ExplSyntaxOff
\begin{document}
\sort{15,4,12,5,45,5,6,15}\par
\sort{12,5,4,5,6,78,100,105}\par
\sort{42,0,-3,5,25,68}
\end{document}

输出
Image


关注,点赞都是我的动力鸭~~~

本文介绍LaTeX3的 prop 模块
简介:主要用来设置一些键值(keyval),但是与l3keys模块又有区别

创建和初始化属性列表

新建一个属性列表

1
2
\prop_new:N \l_my_prop
\prop_new:c {l_my_prop}

初始化(清空内容)

1
2
3
4
\prop_clear:N \l_my_prop
\prop_clear:c ...
\prop_gclear:N ...
\prop_gclear:c ...

检测是否存在,如果存在就清空.如果不存在就创建,然后清空

1
2
3
4
\prop_clear_new:N \l_my_prop
\prop_clear_new:c
\prop_gclear_new:N
\prop_gclear_new:c

赋值

直接赋值

1
2
3
4
5
6
7
8
9
10
\prop_set_from_keyval:Nn \l_my_prop
{
key1 = val1,
key2 = val2,
key3 = val3,
...
}
\prop_set_from_keyval:cn
\prop_gset_from_keyval:Nn
\prop_gset_from_keyval:cn

创建副本

这让 \l_myb_prop\l_mya_prop 相等

1
\prop_set_eq:NN \l_mab_prop \l_mya_prop

常量

1
2
3
4
5
6
7
\prop_const_from_keyval:Nn\l_my_prop
{
key1 = val1,
key2 = val2,
key3 = val3,
...
}

追加(put)

直接追加

1
\prop_put:Nnn \l_my_prop {key4}{val4}

先检测键值是否存在,如果存在,则什么也不做,不存在就为其追加. 区别于前者的是,如果存在,前者会覆盖掉

1
\prop_put_if_new:Nnn \l_my_prop {key4}{val4}

结合

将后面两个属性列表组合起来,给第一个列表

1
\prop_concat:NNN \l_mya_prop \l_myb_prop \l_myc_prop

以键值形式追加

1
2
3
4
5
6
\prop_put_from_keyval:Nn \l_my_prop
{
⟨key1⟩ = ⟨value1⟩ ,
⟨key2⟩ = ⟨value2⟩ ,
...
}

使用

传递给一个变量

1
\prop_get:NnN \l_my_prop {key1} \l_my_tl

此时,\l_my_tl中保存的值就是val1

直接使用

1
\prop_item:Nn \l_my_prop {key1} % ---> val1

计算属性列表数量

1
\prop_count:N \l_my_prop % 返回一个整数

移除

1
\prop_remove:Nn \l_my_prop {key2}

条件判断

检测是否存在

1
2
\prop_if_exist_p:N \l_my_prop
\prop_if_exist:NTF \l_my_prop {true code}{false code}

检测是否为空

1
2
\prop_if_empty_p:N \l_my_prop
\prop_if_empty:NTF \l_my_prop {true code}{false code}

检测某键值是否存在

1
\prop_if_in:NnTF \l_my_prop{key4}{true code}{false code}

循环

遍历

keysvals通过参数传递给函数,所以函数应当接受两个参数

1
\prop_map_function:NN \l_my_prop \my_cmd:nn

example

1
2
3
4
5
6
7
8
9
\cs_new:Nn \my_cmd:nn {#1~:~i~love~#2.\par}
\prop_new:N \l_my_prop
\prop_set_from_keyval:Nn \l_my_prop
{
a = 杨幂,
b = 迪丽热巴,
c = 周冬雨
}
\prop_map_function:NN \l_my_prop \my_cmd:nn

这会得到

1
2
3
a : i love 杨幂.
b : i love 迪丽热巴.
c : i love 周冬雨.
1
\prop_map_inline:Nn \l_my_prop {code}

code中可以带两个参数,用于代表keyval

example

1
2
3
4
5
6
7
8
\prop_new:N \l_my_prop
\prop_set_from_keyval:Nn \l_my_prop
{
a = 杨幂,
b = 迪丽热巴,
c = 周冬雨
}
\prop_map_inline:Nn \l_my_prop{#1~:~i~love~#2.\par}

同样也得到上面的结果

break

用于终止循环

1
\prop_map_break:

下面这个函数会在终止循环后再执行一些东西

1
\prop_map_break:n {code}

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
\prop_new:N \l_my_prop
\prop_set_from_keyval:Nn \l_my_prop
{
a = 杨幂,
b = 迪丽热巴,
c = 周冬雨
}
\prop_map_inline:Nn \l_my_prop
{
\str_if_eq:nnTF{#1}{c}
{
\prop_map_break:
}
{
#1~:~i~love~#2.\par
}
}

这将只得到

1
2
a : i love 杨幂.
b : i love 迪丽热巴.

该模块暂时更新到这里吧

本文仅介绍 LaTeX3 整数数组模块


区别于其它语言的一点:数组首个元素编号是 1, 而不是 0.

构建数组

构建一个长度为 5 的数组 \l_array_int

1
\intarray_new:Nn \l_array_int {5}

数组赋值

给数组第 3 个元素赋值为 5.

1
\intarray_gset:Nnn \l_array_int {3}{5}

整体赋值

可以不用提前声明数组长度

1
\intarray_const_from_clist:Nn \l_array_int {2,5,4,1,6}

获取数组长度

1
2
\intarray_const_from_clist:Nn \l_array_int {2,5,4,1,6,10,5}
\intarray_count:N \l_array_int %---> 7

使用数组

1
\intarray_item:Nn \l_array_int {3} %---> 4
1
\intarray_rand_item:N \l_array_int %随机输出一个元素

初始化数组

1
\intarray_gzero:N \l_array_int

其它

1
2
\intarray_show:N ...
\intarray_log:N ...

点赞,收藏都是我的动力鸭~~~

本文仅仅介绍 LaTeX3 的整数模块.


构建赋值、运算

首先,利用以下函数创建一个整数变量

1
2
\int_new:N \l_my_int
\int_new:c {l_my_int}

这样就创建了一个整数变量,下面我们来为其赋值:

1
2
3
\int_set:Nn \l_my_int {10} % --->10
\int_set:Nn \l_my_int {2+2*3} % ---->8
\int_gset:Nn \l_my_int {2+2*3+(2+1)*3} % --->17
1
2
\int_set_eq:NN \l_my_int_b \l_my_int_a
\int_gset_eq:NN \l_my_int_b \l_my_int_a

使用整数

1
2
\int_use:N \l_my_int
\int_use:c {...}

加(减)一个数

1
2
3
4
5
 % \int_set:Nn \l_my_int {3}
\int_add:Nn \l_my_int {10}
\int_use:N \l_my_int % --->13
\int_sub:Nn \l_my_int {1+9}
\int_use:c {l_my_int} %--->3

自加(减) 1

1
2
\int_incr:N \l_my_int
\int_decr:N \l_my_int

运算

1
\int_eval:n {10+2*3+5} % --->21

一些数学函数

1
2
3
4
5
6
7
8
\int_sign:n {100} % --->1 ,符号函数
\int_abs:n {-10} % --->10 ,绝对值
\int_div_round:nn {3}{1+1} % --->2 ,除法,四舍五入
\int_div_truncate:nn {3}{2} % --->1,除法,向零取整
\int_max:nn {100}{45} % --->100,取较大的一个
\int_min:nn {100}{45} % --->45
\int_mod:nn {9}{5} % --->4 ,取余数
\int_zero:N \l_my_int % ---> ,将变量清零

测试、比较

测试变量是否存在

1
2
\int_if_exist_p:N \l_my_int %--->输出布尔值 
\int_if_exist:NTF \l_my_int {yes} {no} %--->条件语句

比较

1
2
3
4
\int_compare_p:nNn {100} > {102} 
\int_compare:nNnTF {100} > {102} {yes} {no} % ---> no
\int_compare_p:n {1<2<3}
\int_compare:nTF {1<2<3} {yes} {no}

以上除了 >, <, 还有 =, !=, >= and <= .

case

1
2
3
4
5
6
\int_case:nn{10}
{
{2+4}{aaa}
{3+7}{bbb}
} % ---> bbb
\int_case:nnTF ...

测试奇偶

1
2
3
4
\int_if_even_p:n {...}
\int_if_even:nTF {...} {...} {...}
\int_if_odd_p:n {...}
\int_if_odd:nTF {...} {...} {...}

循环

do-until 循环

1
\int_do_until:nNnn {<intexpr1>} <relation> {<intexpr2>} {<code>}

这是 do-until 循环, 它会先执行一次 <code> 中的内容,然后循环,直到条件为真.

示例

1
2
3
4
5
6
7
8
9
\int_new:N \l_mya_int
\int_new:N \l_myb_int
\int_set:Nn \l_mya_int {1}
\int_set:Nn \l_myb_int {10}
\int_do_until:nNnn {\l_mya_int}>{\l_myb_int}
{
this~is~\int_use:c{l_mya_int}.
\int_incr:N \l_mya_int \par
}

这将会输出

示例

1
2
3
4
5
6
7
8
9
\int_new:N \l_mya_int
\int_new:N \l_myb_int
\int_set:Nn \l_mya_int {1}
\int_set:Nn \l_myb_int {10}
\int_do_until:nNnn {\l_mya_int}<{\l_myb_int}
{
this~is~\int_use:c{l_mya_int}.
\int_incr:N \l_mya_int \par
}

这将只得到 this is 1. 对比一下.

do-while 循环

1
\int_do_while:nNnn {<intexpr1>} <relation> {<intexpr2>} {<code>}

do-until 循环类似,不过它是等到条件为假时结束循环

示例

1
2
3
4
5
6
7
8
9
\int_new:N \l_mya_int
\int_new:N \l_myb_int
\int_set:Nn \l_mya_int {1}
\int_set:Nn \l_myb_int {10}
\int_do_while:nNnn {\l_mya_int}<{\l_myb_int}
{
this~is~\int_use:c{l_mya_int}.
\int_incr:N \l_mya_int \par
}

这将会输出

类似的还有 until-do , while-do 循环,它们是直接测试条件,不会事先执行一次 <code> 中的内容.

1
2
3
4
5
6
7
8
9
10
11
\int_until_do:nNnn {⟨intexpr1⟩} ⟨relation⟩ {⟨intexpr2⟩} {⟨code⟩}

\int_while_do:nNnn {⟨intexpr1⟩} ⟨relation⟩ {⟨intexpr2⟩} {⟨code⟩}

\int_do_until:nn {⟨integer relation⟩} {⟨code⟩}

\int_do_while:nn {⟨integer relation⟩} {⟨code⟩}

\int_until_do:nn {⟨integer relation⟩} {⟨code⟩}

\int_while_do:nn {⟨integer relation⟩} {⟨code⟩}

自行测试.

step (按步)循环

1
\int_step_function:nN {⟨final value⟩} ⟨function⟩

这个函数是接受 nN 变体,第一个接受一个整数,第二个接受一个函数,也即将整数作为参数传递给该函数.

示例

1
2
\cs_set:Nn \l_my_cmd:n {I~am~#1~years~old.\par }
\int_step_function:nN {6} \l_my_cmd:n

这将会得到

1
2
\cs_set:Nn \l_my_cmd:n {I~am~#1~years~old.\par }
\int_step_function:nnN {5}{8} \l_my_cmd:n

1
2
\cs_set:Nn \l_my_cmd:n {I~am~#1~years~old.\par }
\int_step_function:nnnN {1}{2}{9} \l_my_cmd:n

以上是按函数循环,下面还有两种循环

1
\int_step_inline:nn{5}{我有~#1~个苹果 \par}

得到

这是用 #1 作为参数传递给后面的内容,另外还有:

1
2
3
4
5
6
7
\int_step_inline:nnn{3}{7}{我有~#1~个苹果 \par}

\int_step_inline:nnnn{3}{2}{9}{我有~#1~个苹果 \par}

\int_step_variable:nNn ...
\int_step_variable:nnNn ...
\int_step_variable:nnnNn ...

自行测试一下.

转换

1
2
3
\int_to_arabic:n{123} % --->123
\int_to_alph:n{3} % --->c
\int_to_Alph:n{4} % --->D
1
2
3
4
5
6
7
8
9
10
\cs_new:Npn \int_to_alph:n #1
{
\int_to_symbols:nnn {#1} { 26 }
{
{ 1 } { a }
{ 2 } { b }
...
{ 26 } { z }
}
}
1
2
3
4
5
6
7
\int_to_bin:n{6} % --->110 ,二进制
\int_to_hex:n{78} % --->4e ,十六进制
\int_to_Hex:n{78} % --->4E ,十六进制
\int_to_oct:n{10} % --->12 ,八进制
\int_to_base:nn{9}{3} % --->100 ,三进制
\int_to_base:nn{9}{4} % --->21 ,四进制
\int_to_Base:nn{17}{18} % --->H ,十八进制
1
2
\int_to_roman:n {2} % ---> ii 
\int_to_Roman:n {2} % ---> II
1
2
3
4
5
6
\int_from_alph:n {f} % ---> 6
\int_from_bin:n ...
\int_from_hex:n ...
\int_from_oct:n ...
\int_from_roman:n ...
\int_from_base:nn ...

伪随机数

1
2
\int_rand:n {10} % 1~10 之间产生一个随机整数
\int_rand:nn {5}{8} % 5~8 之间产生一个随机整数

已定义的常量

1
2
3
4
5
\c_zero_int % --->0
\c_one_int % --->1
\c_max_int % ---> 2147483647 ,可存储的最大整数
\c_max_register_int % --->32767 ,整数寄存器的最大数量
\c_max_char_int % --->255 ,引擎完全支持的最大字符编码。

就到这儿吧,整数模块就告一段落了.

使用LaTeX3实现简单的函数递归,并计算正整数的阶乘和斐波那契数列

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
\cs_set:Npn \froac:n #1{
\int_compare:nNnTF{#1}={0}{1}
{
\fp_eval:n{(#1)*\froac:n{#1-1}}
}
}
\cs_set:Npn \fibon:n #1{
\int_compare:nNnTF{#1}={0}{0}
{
\int_compare:nNnTF{#1}={1}{1}
{
\fp_eval:n{\fibon:n{#1-1}+\fibon:n{#1-2}}
}
}
}

输出接口

1
2
3
4
5
6
\NewDocumentCommand{\fact}{m}{
#1!~=~\froac:n{#1}\par
}
\NewDocumentCommand{\fibon}{m}{
F(#1)~=~\fibon:n{#1}\par
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
\documentclass{ctexart}
\usepackage{mathtools,amsmath}
\usepackage[paperwidth=18cm,margin=3cm]{geometry}
\ExplSyntaxOn
\cs_set:Npn \froac:n #1{
\int_compare:nNnTF{#1}={0}{1}
{
\fp_eval:n{(#1)*\froac:n{#1-1}}
}
}
\cs_set:Npn \fibon:n #1{
\int_compare:nNnTF{#1}={0}{0}
{
\int_compare:nNnTF{#1}={1}{1}
{
\fp_eval:n{\fibon:n{#1-1}+\fibon:n{#1-2}}
}
}
}
\NewDocumentCommand{\fact}{m}{
#1!~=~\froac:n{#1}\par
}
\NewDocumentCommand{\fibon}{m}{
F(#1)~=~\fibon:n{#1}\par
}
\ExplSyntaxOff
\begin{document}
\title{\LaTeX3---使用递归}
\author{ljguo1020@gmail.com}
\maketitle
\ExplSyntaxOn
计算~$n!$~
\[
n!=
\begin{dcases}
1,&n=0\\
n(n-1)!,&n\geq ~ 1
\end{dcases}
\]\par
\int_step_function:nN{20}\fact
\newpage
计算斐波那契数列
\[
F(n)=
\begin{dcases}
0,&n=0\\
1,&n=1\\
F(n-1)+F(n-2),&n\geq 2
\end{dcases}
\]\par
\int_step_function:nN{20}\fibon
\ExplSyntaxOff
\end{document}

输出
Image
Image


关注,点赞都是我的动力鸭~~~

本文将要利用 xparse 宏包和 LaTeX3l3keys 模块来构建一个简单的多参数命令

提出问题

LaTeX 中,一个命令最多接受 9 个参数,虽然在日常使用中远远够了,但还是有一些用户有这方面的需求,我们下面来举一个简单的例子

1
2
3
4
5
6
\newcommand{\rectangle}[4]
{
\begin{tikzpicture}
\draw[line width=#3,draw=#4](0,0) rectangle (#1,#2);
\end{tikzpicture}
}

创建了一个画矩形的命令 \rectangle , 它接受 4 个参数分别是 \rectangle{<width>}{<height>}{<line width>}{<color>} 也即是 宽、高、线宽、颜色 ,这样虽然是合法的,但是难免显得有些臃肿,如果参数再多几个,久而久之,参数顺序估计都忘记了.

讨论问题

首先,我们应该放弃前面的思维方式,然后使用一种更好的方法来解决它,观察如下代码

1
2
3
4
5
6
7
8
\rectangle
{
width=2cm,
height=3cm,
linewidth=1pt,
color=red,
...
}

这样,这个命令仅接受一个参数,但效果却比上面的好得多,我们不用去记住参数顺序,而且可以为每个参数设置一个默认值,当缺省时,就使用默认值. 以上就是所谓的 key = val 键值对.

解决问题

xparse 宏包简介

xparse 宏包是基于 LaTeX3 编写的一个更加强大的创建命令或环境的宏包,它提供了\NewDocumentCommand 用于取代原 LaTeX2e 中的 \newcommand ,例如

1
2
\NewDocumentCommand{\foo}{m}{my name is #1.}
\foo{ljguo}

部分参数介绍

  • m : 标准必选参数

  • o : 标准可选参数 , 如果缺省,会返回一个 -NoValue- 标记

  • O{default} : 同 o ,但可以带一个默认值

  • s : 带星号命令

  • v : 抄录参数

几个命令介绍

  • \IfNoValueTF , \IfNoValueT , \IfNoValueF 用于判断所接受参数是否为 -NoValue- 标记

  • \IfBooleanTF , \IfBooleanT , \IfBooleanF 用于判断所接受参数的逻辑值,主要用于 s 类参数

使用样例

1
2
\NewDocumentCommand{\foo}{m}{my name is #1.}
\foo{ljguo}
1
my name is ljguo.
1
2
3
\NewDocumentCommand{\foo}{o}{my name is #1.}
\foo
\foo[ljguo]
1
2
my name is -NoValue-.
my name is ljguo.
1
2
3
\NewDocumentCommand{\foo}{O{ljguo}}{my name is #1.}
\foo
\foo[latexer]
1
2
my name is ljguo.
my name is latexer.
1
2
3
4
5
6
7
8
\NewDocumentCommand{\foo}{s}
{
\IfBooleanTF{#1}
{i have star.}
{i dont have star.}
}
\foo
\foo*
1
2
i dont have star.
i have star.

l3keys 模块简介

主要用到两个函数

  • \keys_define:nn : 用于设置键值对
  • \keys_set:nn : 用于为键值对赋值
  • 这两个函数的第一个参数接受模块名

注意:使用 LaTeX3 语法应当将该部分代码放在 \ExplSyntaxOn\ExplSyntaxOff 之间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
\ExplSyntaxOn 
\keys_define:nn{example} %为 example 模块设置键值对
{
name.tl_set:N = \l_name_tl, %将 name 保存到一个 tl 变量中
name.default:n = ljguo, %为 name 设置默认值
name.initial:n = ljguo, %为 name 设置初始值
age.int_set:N = \l_age_int, %将 age 保存到一个 int 变量中
age.default:n = 21, %为 age 设置默认值
age.initial:n = 21 %为 age 设置初始值
}
\NewDocumentCommand{\showinfo}{} % 构建一个命令,用于输出个人信息
{
hello,~my~name~is~\tl_use:c {l_name_tl},~and~i~am~\int_use:c {l_age_int} ~ years~old.
}
\showinfo
\keys_set:nn{example} %为 example 模块的键值赋值
{
name = latexer,
age = 50
}
\showinfo
\ExplSyntaxOff
1
2
hello, my name is ljguo, and i am 21 years old.
hello, my name is latexer, and i am 50 years old.

回到原问题

现在我们再来定义一个\rectangle 命令

首先设置键值对,需要四个键值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
\ExplSyntaxOn
\keys_define:nn{rectangle}
{
width.dim_set:N = \l_width_dim,
width.default:n = 2cm,
width.initial:n = 2cm,
height.dim_set:N = \l_height_dim,
height.default:n = 3cm,
height.initial:n = 3cm,
linewidth.dim_set:N = \l_linewidth_dim,
linewidth.default:n = 1pt,
linewidth.initial:n = 1pt,
color.tl_set:N = \l_color_tl,
color.default:n = cyan,
color.initial:n =cyan
}
\NewDocumentCommand{\rectangle}{O{}}
{
\group_begin:
\keys_set:nn{rectangle}{#1}
\begin{tikzpicture}
\draw[line~width = \dim_use:N \l_linewidth_dim,draw = \tl_use:N \l_color_tl](0,0)rectangle(\dim_use:N \l_width_dim,\dim_use:N \l_height_dim);
\end{tikzpicture}
\group_end:
}
\rectangle\par
\rectangle
[
width = 3cm,
height = 2cm,
color = red
]
\ExplSyntaxOff

总结

xparse 宏包是一个非常优秀的宏包,值得大家去学习,另外,作者水平有限,如有错误或者疑问,欢迎联系我

本文只记录函数,也即控制序列(control sequences)的相关用法

与 LaTeX2e 的一些比较

在原始的 LaTeX2e 中 并没有严格的区分变量和函数,或许在命名上有些区分,例如 \mycmd \myvar,它们的定义方法都是一样的

1
2
\newcommand{\mycmd}[2]{#1 love #2}%\mycmd{I}{you}-->I love you
\newcommand{\myvar}{I love \LaTeX}%\myvar -->I love LaTeX

都是用 \newcommand 来定义的,函数无非是多了几个参数,这意味着函数是可以接受参数的,而变量是不需要的.
更加古老的 \def 也是如此

LaTeX3

在LaTeX3中创建一个函数的语法如下:

1
2
\cs_new:Npn \l_my_cmd:nn #1#2 {#1 ~ love ~ #2} 
\l_my_cmd:nn {I}{you} %-->I love you

这是在全局范围内创建一个函数,相当于 \newcommand ,所有如果这个函数已经被定义,将会报错,当然还有其它语法

1
2
\cs_set:Npn \l_my_cmd:nn #1#2 {#1 ~ love ~ #2} %局部创建函数
\cs_gset:Npn \l_my_cmd:nn #1#2 {#1 ~ love ~ #2} %全局创建函数

这两个函数也可以用于创建函数,但是区别于前面的是,如果这个函数存在,它会重新定义该函数,而不会报错,如果不存在,就创建.而这里的 setgset 通过上面的注释,大概知道了,前者用于局部创建,后者用于全局.至于这里的 Npn nn,可以通过上一篇文章了解一下.

前面三个,还有一些情况,待我一一道来

1
\cs_set_nopar:Npn \l_my_cmda:nn #1#2{#1 ~ Do~not ~love ~ #2}

顾名思义 nopar 就是参数中不能加 \par 这个命令,也就是不能分段啦。

1
\cs_set_protected:Npn \l_my_cmd:nn #1#2 {#1 ~ love ~ #2}

同样,根据名字 protected 我们可以猜到,这是一个受保护的函数,也就是它不能被 x 型展开.

1
\cs_set_protected_nopar:Npn \l_my_cmd:nn #1#2 {#1 ~ love ~ #2}

这个,自己猜一下吧。

另外,以上只列举了 set ,当然 new,gset 也都有这些对应的情况,并且它们默认各自还提供了几种不同的变体供大家使用,当然也可以自己创建一些变体,我们后面再讲。

大家再来看看

1
\cs_new:Nn \l_my_cmd:nn {#1 ~ love ~ #2 }

对比一下,是不是有点不一样 Npn $\to$ Nn,少了个屁 (p),读过前面的文章应该知道,这个 p 其实就是 parameters (参数), 创建函数时可以将其省略 ,对应的花括号外部的 #1#2 也一并省去,系统会自动检测参数数量.

1
\cs_set_eq:NN \l_my_cmda:nn \l_my_cmdb:nn

这个函数的作用是复制函数定义,复制第二个函数的定义给第一个函数,在底层 TeX 中有 \let\cmda\cmdb 与之有类似或者一模一样(?)的功能, eq $\to$ equal ,当然 new , gset
也有对应的函数.

1
2
\cs_undefine:N \l_my_cmd:nn
\cs_undefine:c {l_my_cmd:nn}

这个函数 undefine 顾名思义,就是删掉一个函数的定义。例如,我们前文定义了函数 \l_my_cmd:nn 现在我们使用以上其中一个命令 (两个命令是等效的,待会就会讲到), 然后再用该函数的话,系统就会报错

1
2
! Undefined control sequence.
\l_my_cmd:nn

结果很显然了.

. 这里我们第一次遭遇 c 变体, N 变体接受一个 cs (控制序列),也就是一个带反斜杠的命令(?), 而 c 接受一个 (token list) 也就是一个带花括号的内容,然后会为花括号里面的内容自动加上一个 \ , 对比上面的例子,不难理解.

还有一些函数如下,我将它们一起列举出来 (偷个懒):

1
2
3
4
5
6
\cs_meaning:N \l_my_cmd:nn
\cs_meaning:c {l_my_cmd:nn}
\cs_show:N ...
\cs_show:c {...}
\cs_log:N ...
\cs_log:c {...}

这三个函数都是用于查看函数定义的,区别在于第一个是直接在 PDF 里面显示,第二个是在终端显示 (会停止运行) ,第三个是在日志中显示.

1
2
3
4
5
6
%first
\use:c {l_my_cmd:nn}
%second
\cs:w
l_my_cmd:nn
\cs_end

上面两个作用是一样的,前者将内容放在花括号里面,后者放在环境之间.都等价于直接使用 \l_my_cmd:nn .

1
\cs_to_str:N \l_my_cmd:nn

这个函数是将函数转化为字符串 (str) 输出是 ˙my˙cmd:nn,好吧,我也不知道它干嘛用的,希望有大佬能为我解释一下.

1
2
3
\use:n {abc}
\use:nn {abc}{{def}}
\use_i:nn {abc}{def}

上面三个函数得到的结果依次是:abc, abc{def}, abc ,这个函数的作用就是直接将花括号里面的内容输出,应该注意第二个,你在 PDF 里面看到的应该是 abcdef 这是因为 {} 在PDF 里面是不显示的,当然我们可以做一个简单的测试来验证它

1
2
$a^\use:n{abc}$ 
$a^\use:n{{abc}}$

输出依次是 $a^{a}bc$, $a^{abc}$.

1
2
\cs_if_eq_p:NN \l_my_cmda:nn \l_my_cmdb:nn
\cs_if_eq:NNTF \l_my_cmda:nn \l_my_cmdb:nn {yse}{no}

第一个函数用于测试两个函数的定义是否相同,在 PDF 中的显示是 $\Delta$,$\Gamma$ ,这其实就是两个布尔值, 前者为真,后者为假. 第二个函数同样的作用,只不过跟上了 T, F 变体,也就是说条件为真,执行 T,条件为假,执行 F.

1
2
3
4
\cs_if_exist_p:N \l_my_cmd:nn
\cs_if_exist_p:c {...}
\cs_if_exist:NTF ...
\cs_if_exist:cTF {...}

通过名字,可以知道,这是用来测试这个函数是否存在的,类比一下上面就知道了.(懒得敲了)


函数篇大概就写这么多了吧,还有些函数我没看懂,或者暂时不知道它有啥用的,就先不写了,以后再更吧.