论坛首页 综合技术论坛

Programming in Emacs Lisp笔记(十五)准备图表

浏览 2815 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-07-19  

准备图表

我们的目标是构造一个图表显示Emacs lisp源码中所有函数定义的长度范围。

在实际应用中,如果你要创建一个图表,你可能会使用gnuplot之类的程序来完成这个工作。(gnuplot与GNU Emacs集成得很好。)但在这里,我们将使用前面我们所学的知识来完成这个工作。

在这章,我们将先编写一个简单的图表打印函数。第一个版本将作为原型,在此基础上来增强。

打印图表列

由于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个函数和变量。查找这个列表,我们看到有个insert-rectangle函数有可能能完成这个工作。

这个函数文档如下:

insert-rectangle:
Insert text of RECTANGLE with upper left corner at point.
RECTANGLE's first line is inserted at point,
its second line is inserted at a point vertically under point, etc.
RECTANGLE should be a list of strings.

我们可以测试一下,以确认它是否如我们期望的那样工作。

把光标放在insert-rectange语句的后面按C-u C-x C-e(eval-last-sexp)。这个函数将在point的下面插入"first","second","third"。函数返回值为nil。

(insert-rectangle '("first" "second" "third"))first
second
third
nil
在绘制图表的程序中使用这上函数。我们需要先确保point位于需要插入的位置,然后用insert-rectangle函数插入列。

如果你是在Info中读取这个文档,你可以切换到另一个缓冲区,比如*scratch*,将point放在任何地方,输入M-:,在提示区输入insert-rectangle语句,然后回车。Emacs将执行输入的语句,交把*scratch*缓冲区中的point位置作为point的值。(M-:被绑定到eval-expression上。)

我们将发现当执行完成插入后,point被设置在了最后插入的那行,也就是说这个函数移动了point。如果我们重复执行这个命令,下次插入的内容将在上次插入内容的下面。我们并不需要这样,我们需要的是一个柱状图表,一列挨着一列。

我们看出每次while循环插入列时必须重新设置point的位置,这个位置必须在列的顶部,而不是在底部。并且,我们打印图表时,并不需要每个列 都一样高。这意味着每个列的顶部并不是一样高的。我们不能简单在一同一行上执行同一个操作,而是需要先将point移到正确的位置。

我们准备用星号来描述柱状图。星号的数量取决于当前numbers-list中元素的值。我们需要构造一个包含星号的列表以便insert- rectangle来画出正确高度的列。如果这个list只包含一定数量的星号,那我们就必须在绘制前将point设置到正确的高度。这比较困难。

我们可以想出另外一种方式,每次传递给insert-rectangle一个同样长度的list,它们可以在同一行插入,每次插入时只需要向右移动一列。比如,如果最高的高度为5,但实际高度只有3,则insert-rectangle需要的参数如下:

(" " " " "*" "*" "*")

最后一个需求不是很难,我们需要决定列的高度。有两种方法:我们可以使用任意的值或使用整个list中最大的数字作为最大高度值。Emacs中提供了内置的函数检查参数中的最大值。我们可以使用这个函数。这个函数被称为max它返回它所有参数中的最大值。例:

(max  3 4 6 5 7 3)

将返回7。(相反的函数是min它返回参数中最小的值)

但是,我们不能简单的在numbers-list上调用max;max函数需要数字类型的参数,而不是包含数字的list。因此,下面的语句:

(max  '(3 4 6 5 7 3))
将出错:
Wrong type of argument:  number-or-marker-p, (3 4 6 5 7 3)

我们需要一个函数将list拆开作为参数传递给函数。这个函数是apply。这个函数将其它的参数传递给它的第一个参数,它的最后一个参数可以是一个list。

例如:

(apply 'max 3 4 7 3 '(4 8 5))
将返回8。

(顺便说一句,我不知道你如何学习书本上没有介绍过的函数。可以根据函数名称,比如search-forward或insert-rectangle,根据他们的部分名称使用apropos查找函数的相关信息。)

传递给apply的第二个参数是可选参数,我们可以使用aplly调用一个函数并将list中的元素传递给这个函数,比如下面的代码也将返回8:

(apply 'max '(4 8 5))
后面我们将使用apply。函数recursive-lengths-list-many-files返回包含数字的list,我们对其调用max。

这样,查找图表中的最大数量的代码如下:

(setq max-graph-height (apply 'max numbers-list))

现在我们回到如何构造包含列图表字符串的list的问题上。知道图表的最大高度和星号的数量后,函数应该可以返回一个传递给insert-rectangle的list了。

每一列由星号或空格构成。因为函数传递了列高度和列中的星号数量两个参数,空白的数量应该是高度减去星号数量。给出空白数量和星号数量后,两个循环可以构造出这个list:

;;; First version.
(defun column-of-graph (max-graph-height actual-height)
"Return list of strings that is one column of a graph."
(let ((insert-list nil)
(number-of-top-blanks
(- max-graph-height actual-height)))

;; Fill in asterisks.
(while (> actual-height 0)
(setq insert-list (cons "*" insert-list))
(setq actual-height (1- actual-height)))

;; Fill in blanks.
(while (> number-of-top-blanks 0)
(setq insert-list (cons " " insert-list))
(setq number-of-top-blanks
(1- number-of-top-blanks)))

;; Return whole list.
insert-list))
安装这个函数后,执行下面的代码:
(column-of-graph 5 3)
将返回:
(" " " " "*" "*" "*")

如上面所写,column-of-graph包含一个瑕疵:用于标识空白和列的符号是硬编码的,使用了空白和星号。这是一个很好的原型,如果其它人 想换成其它的符号。比如用逗号代替空白,用加号代替星号等。程序应该更具弹性一些。应该使用两个变量来代替空白和星号:将graph-blank和 graph-symbol定义为两个独立的变量。

上面也没有编写文档。我们可以编写这个函数的第二个版本:

(defvar graph-symbol "*"
"String used as symbol in graph, usually an asterisk.")

(defvar graph-blank " "
"String used as blank in graph, usually a blank space.
graph-blank must be the same number of columns wide
as graph-symbol."
)

;;(For an explanation of defvar, see Initializing a Variable with defvar.)

;;; Second version.
(defun column-of-graph (max-graph-height actual-height)
"Return MAX-GRAPH-HEIGHT strings; ACTUAL-HEIGHT are graph-symbols.
The graph-symbols are contiguous entries at the end
of the list.
The list will be inserted as one column of a graph.
The strings are either graph-blank or graph-symbol."


(let ((insert-list nil)
(number-of-top-blanks
(- max-graph-height actual-height)))

;; Fill in graph-symbols.
(while (> actual-height 0)
(setq insert-list (cons graph-symbol insert-list))
(setq actual-height (1- actual-height)))

;; Fill in 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)))

;; Return whole list.
insert-list))

如果需要,我们可以再次重写column-of-graph,使用线型图表代替柱状图表。这不会很困难。其中一个办法就是让柱状图中第一个星号以下的显示 为空白。在构造线型图表的一个列时,函数首先构造一个空的list,长度比元素的值小1,然后用cons将符号和列表连接;然后再次使用cons将顶部用 空白填充。

现在,我们终于完成第一个打印图表的函数。它只打印了图表的body部分,而没有水平和垂直方向的轴,因此我们把这个函数称为graph-body-print。

graph-body-print函数

上一节,graph-body-print函数完成了打印图表列的功能。这应该是一个重复执行的动作。我们可以使用递减的while循环或递归函数来完成这些操作。这节,我们使用while循环来编写函数定义。

column-of-graph函数需要图表高度作为参数,因此我们需要决定图表高度并将它保存到一个局部变量中。

我们的使用while循环的函数模板如下:

(defun graph-body-print (numbers-list)
"documentation..."
(let ((height ...
...))

(while numbers-list
insert-columns-and-reposition-point
(setq numbers-list (cdr numbers-list))))

我们需要填空。

我们可以用(apply 'max numbers-list) 获取图表的高度。

while循环遍历numbers-list。并用(setq numbers-list (cdr numbers-list))截短它。每次list的CAR值,就是传递给column-of-graph的参数。

每个循环周期中,insert-rectangle函数使用column-of-graph插入list。由于insert-rectangle函 数将point移到了插入的矩形区域的右下解,我们需要保存当前point的位置,在插入矩形区域后恢复point的位置,然后将point水平移动到下 一个列,并再次调用insert-rectangle。

如果被插入的列是一个字符宽(比如星号或一个空格),这个命令比较简单(forward-char 1);但如果列宽超过1。这时命令需要写为(forward-char symbol-width)symbol-width是graph-blank的长度,可以使用(length graph-blank)。可以在let语句的变量列表中设置symbol-width变量。

函数定义如下:

(defun graph-body-print (numbers-list)
"Print a bar graph of the NUMBERS-LIST.
The numbers-list consists of the Y-axis values."


(let ((height (apply 'max numbers-list))
(symbol-width (length graph-blank))
from-position)

(while numbers-list
(setq from-position (point))
(insert-rectangle
(column-of-graph height (car numbers-list)))
(goto-char from-position)
(forward-char symbol-width)
;; Draw graph column by column.
(sit-for 0)
(setq numbers-list (cdr numbers-list)))
;; Place point for X axis labels.
(forward-line height)
(insert "\n")
))

这里出现了一个新的函数(sit-for 0)。这个语句将使Emacs重绘屏幕。放在这里,Emacs将一列列的绘制。如果没有,Emacs在函数退出前都不会绘制。

我们可以使用一个较短的包含数字的list来测试graph-body-print。

  1. 安装graph-symbol,graph-blank,column-of-graph,graph-body-print。
  2. 复制下面的语句:
(graph-body-print '(1 2 3 4 6 4 3 5 7 6 5 2 3))
  1. 切换到*scratch*缓冲区并把光标放置在要绘制的开始位置。
  2. 输入M-:(eval-expresion)
  3. Yank(C-Y) graph-body-print语句到缓冲区中。
  4.  回车执行graph-body-print语句。

Emacs将打印出下面的图表:

                    *
* **
* ****
*** ****
********* *
************
*************

recursive-graph-body-print函数

graph-body-print函数也可以用递归来编写。递归分解为两个部分:外部使用let包装,决定几个变量的值,比如图表最大高度,内部的函数调用是递归调用,用于打印图表,

包装部分不复杂:

(defun recursive-graph-body-print (numbers-list)
"Print a bar graph of the NUMBERS-LIST.
The numbers-list consists of the Y-axis values."

(let ((height (apply 'max numbers-list))
(symbol-width (length graph-blank))
from-position)
(recursive-graph-body-print-internal
numbers-list
height
symbol-width)))

递归函数部分有点复杂。它有四个部分:'do-again-test'打印操作的代码,递归调用,'next-step-expression'。 'do-again-test'是一个if语句用于检查numbers-list是否还有元素,如果有函数将使用打印操作的代码打印一个列,并再次调用自 身。函数调用自身时'next-step-expressin'将截短numbers-list。

(defun recursive-graph-body-print-internal
(numbers-list height symbol-width)
"Print a bar graph.
Used within recursive-graph-body-print function."


(if numbers-list
(progn
(setq from-position (point))
(insert-rectangle
(column-of-graph height (car numbers-list)))
(goto-char from-position)
(forward-char symbol-width)
(sit-for 0) ; Draw graph column by column.
(recursive-graph-body-print-internal
(cdr numbers-list) height symbol-width))))

在安装这个函数后,可以用下面的例子测试:

(recursive-graph-body-print '(3 2 5 6 7 5 3 4 6 4 3 2 1))
结果如下:
                *
** *
**** *
**** ***
* *********
************
*************
论坛首页 综合技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics