由于Emacs被设计为灵活且适用于各种终端,包括字符终端,图表需要由打字机符号之一制作。 星号就可以;随着图表打印函数的改进,我们可以选择使用用户选项来指定符号。
我们可以将这个函数称为graph-body-print
;它将以numbers-list
作为其唯一参数。
在这个阶段,我们不会为图表标记,而只会打印其主体。
graph-body-print
函数为numbers-list
中的每个元素插入一个垂直的星号列。
每行的高度由numbers-list
的该元素的值确定。
插入列是一种重复性的操作;这意味着可以使用while
循环或递归来编写此函数。
我们的第一个挑战是发现如何打印星号列。通常,在Emacs中,我们通过逐行输入以水平方式将字符打印到屏幕上。 我们有两条路可以走:编写我们自己的列插入函数或发现Emacs中是否存在这样一个函数。
为了查看Emacs中是否存在这样的函数,我们可以使用M-x apropos命令。此命令类似于C-h a(command-apropos
)命令,
但后者仅找到命令函数。M-x apropos命令列出与正则表达式匹配的所有符号,包括不是交互式的函数。
我们要寻找的是某个打印或插入列的命令。很可能,该函数的名称将包含“print”或“insert”或“column”这个词。
因此,我们可以简单地输入M-x apropos RET print\|insert\|column RET并查看结果。
在我的系统上,这个命令曾经花费了相当长的时间,然后产生了一个包含79个函数和变量的列表。现在它几乎不花费任何时间,
并生成包含211个函数和变量的列表。在列表中扫描,唯一看起来可能完成这项工作的函数是insert-rectangle
。
事实上,这正是我们想要的函数;其文档如下:
insert-rectangle: 在点的上左角插入RECTANGLE的文本。 RECTANGLE的第一行插入在点处, 其第二行插入在点的下面的垂直位置,依此类推。 RECTANGLE应该是一个字符串列表。 此命令执行后,标记位于左上角, 点位于右下角。
我们可以进行一次快速测试,确保它能够按照我们的期望工作。
在将光标放在insert-rectangle
表达式之后,键入C-u C-x C-e(eval-last-sexp
),
以下是在点和下面插入了字符串‘"first"’、‘"second"’和‘"third"’的函数结果。
此外,该函数返回nil
。
(insert-rectangle '("first" "second" "third"))first second thirdnil
当然,我们不会将insert-rectangle
表达式本身的文本插入我们正在创建图表的缓冲区,
而是将从我们的程序中调用该函数。但是,我们必须确保点在缓冲区中insert-rectangle
函数将插入其列的位置。
如果您在Info中阅读此内容,您可以通过切换到另一个缓冲区,例如*scratch*缓冲区,
将点放在缓冲区的某个位置,键入M-:,在提示符处键入insert-rectangle
表达式,
然后键入RET。这会导致Emacs在小缓冲区中评估表达式,但将点的值用作*scratch*缓冲区中点的位置的值。
(M-:是eval-expression
的键绑定。此外,由于表达式在小缓冲区中评估,因此nil
不会出现在*scratch*缓冲区中,
因为表达式在小缓冲区中评估。)
当我们这样做时,我们发现点最终位于最后插入行的末尾——也就是说,此函数将点作为副作用移动了。 如果我们在此位置重复命令,将点放在此位置,下一个插入将在上一个插入的下方和右侧。 我们不想要这个!如果我们要制作一个条形图,那么列需要相互并列。
因此,我们发现列插入while
循环的每个周期必须将点重新定位到我们想要的位置,
并且该位置将位于列的顶部而不是底部。此外,我们记得当打印图表时,我们不希望所有列的高度都相同。
这意味着每个列的顶部可能与前一个列的顶部高度不同。我们不能简单地将点重新定位到相同的行,而是可能移到右边……。
我们计划使用星号制作条形图的列。列中的星号数量是由numbers-list
的当前元素指定的数量。
我们需要构建一个合适长度的星号列表,以便每次调用insert-rectangle
时都会插入正确的位置。
如果此列表仅包含所需数量的星号,则必须为图的打印正确位置将点移动到图的基础上方的正确行。 这可能会很困难。
另一方面,如果我们可以找到一种方法,将insert-rectangle
传递一个每次都是相同长度的列表,
那么我们就可以每次都将点放在相同的行上,但对于每个新列都将其右移一列。如果这样做,那么传递给insert-rectangle
的列表中的一些条目必须是空白,而不是星号。
例如,如果图的最大高度为5,但列的高度为3,则insert-rectangle
需要一个参数,如下所示:
(" " " " "*" "*" "*")
最后的提议并不那么困难,只要我们能确定列高度。我们有两种方法来指定列高度:我们可以任意规定它将是多少,这对于该高度的图表是可以的;
或者我们可以搜索数字列表,将其最大高度用作图的最大高度。如果后者的操作很困难,那么前者的过程将是最简单的,
但是Emacs内置了一个确定其参数的最大值的函数。我们可以使用该函数。该函数称为max
,它返回其参数的最大值,这些参数必须是数字。例如,
(max 3 4 6 5 7 3)
但我们不能简单地在numbers-list
上调用max
函数;max
函数期望数字作为其参数,而不是数字列表。
因此,以下表达式,
(max '(3 4 6 5 7 3))
产生以下错误消息:
Wrong type of argument: number-or-marker-p, (3 4 6 5 7 3)
我们需要一个函数,将参数列表传递给函数。这个函数是apply
。该函数将其第一个参数(函数)应用于其余的参数,
其中最后一个参数可以是一个列表。
例如,
(apply 'max 3 4 7 3 '(4 8 5))
返回8。
(顺便说一下,如果没有像这样的书,我不知道您将如何了解此函数。通过猜测其名称的一部分,然后使用apropos
,
可以发现其他函数,如search-forward
或insert-rectangle
。尽管它在隐喻上的基础很清晰——将其第一个参数应用于其余部分——但我怀疑初学者在使用apropos
或其他工具时是否会想到使用这个特定的词。当然,我可能是错的;毕竟,首次命名该函数的人必须发明它。)
apply
的第二个和后续的参数是可选的,因此我们可以使用apply
来调用一个函数并将其列表元素传递给它,
就像这样,它也返回8:
(apply 'max '(4 8 5))
我们将使用apply
的这种方式。recursive-lengths-list-many-files
函数返回一个数字列表,
我们可以将max
应用于它(我们也可以将max
应用于排序后的数字列表;列表是否排序都无关紧要)。
因此,找到图的最大高度的操作是这样的:
(setq max-graph-height (apply 'max numbers-list))
现在我们可以回到创建图表列的问题。告诉了图的最大高度和列中星号的数量后,
函数应返回insert-rectangle
命令插入的列表。
每一列由星号或空格组成。由于函数传递了列的高度和列中星号的数量,因此可以通过从列的高度中减去星号的数量来找到空格的数量。给定空格的数量和星号的数量,可以使用两个 while
循环构造列表:
;;; 第一个版本。
(defun column-of-graph (max-graph-height actual-height)
"返回一个图表列的字符串列表。"
(let ((insert-list nil)
(number-of-top-blanks
(- max-graph-height actual-height)))
;; 填充星号。
(while (> actual-height 0)
(setq insert-list (cons "*" insert-list))
(setq actual-height (1- actual-height)))
;; 填充空格。
(while (> number-of-top-blanks 0)
(setq insert-list (cons " " insert-list))
(setq number-of-top-blanks
(1- number-of-top-blanks)))
;; 返回整个列表。
insert-list))
如果安装了此函数,然后评估以下表达式,您将看到它按预期返回列表:
(column-of-graph 5 3)
返回
(" " " " "*" "*" "*")
如上所写,column-of-graph
包含一个主要缺陷:用于空白和列中标记条目的符号是硬编码的,分别是空格和星号。这对于原型来说是可以的,但您或其他用户可能希望使用其他符号。例如,在测试图函数时,您可能想要使用句点代替空格,以确保每次调用 insert-rectangle
函数时点被正确重新定位;或者您可能想要用 ‘+’ 符号或其他符号替换星号。您甚至可能希望创建一个宽度超过一个显示列的图表列。程序应该更加灵活。为了实现这一点,我们可以用两个变量 graph-blank
和 graph-symbol
替换空白和星号,并分别定义这两个变量。
此外,文档写得不好。这些考虑因素引导我们到函数的第二个版本:
(defvar graph-symbol "*" "用于图表的符号字符串,通常是星号。")
(defvar graph-blank " " "用于图表的空白字符串,通常是空格。 graph-blank 的宽度必须与 graph-symbol 相同。")
(有关 defvar
的解释,请参见 用 defvar
初始化变量。)
;;; 第二个版本。
(defun column-of-graph (max-graph-height actual-height)
"返回 MAX-GRAPH-HEIGHT 个字符串;ACTUAL-HEIGHT 是图表符号。
图表符号是列表末尾的连续条目。 列表将作为图表的一列插入。 这些字符串要么是 graph-blank,要么是 graph-symbol。"
(let ((insert-list nil) (number-of-top-blanks (- max-graph-height actual-height)))
;; 填充 graph-symbols
。
(while (> actual-height 0)
(setq insert-list (cons graph-symbol insert-list))
(setq actual-height (1- actual-height)))
;; 填充 graph-blanks
。
(while (> number-of-top-blanks 0)
(setq insert-list (cons graph-blank insert-list))
(setq number-of-top-blanks
(1- number-of-top-blanks)))
;; 返回整个列表。
insert-list))
如果我们愿意,我们可以再次重写 column-of-graph
,以提供线图和条形图的可选支持。这不难做到。想象一下线图不过是每个条的顶部以下部分为空格的条形图。为了构建线图的列,该函数首先构建一个比值短一的空格列表,然后使用 cons
将图符附加到列表;然后再次使用 cons
将顶部空格附加到列表。
容易看出如何编写这样的函数,但由于我们不需要它,我们将不予实现。但是这项工作是可以完成的,如果完成了,将使用 column-of-graph
完成。更重要的是,几乎不需要在任何其他地方进行更改。如果我们有兴趣进行增强,这是个简单的任务。
现在,最后,我们来到我们的第一个实际图表打印函数。这将打印图表的主体,而不是垂直和水平轴的标签,因此我们可以称其为 graph-body-print
。