精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2007-07-19
计数:重复和正则表达式重复执行和正则表达式是Emacs Lisp中非常强大的工具。这章讲解使用while循环和递归结合正则表达式进行查找进行字数统计。 字数统计标准的Emacs发行版中包含了一个统计region中行数的函数。但没有统计字数的函数。 count-words-region 函数字数统计函数可以统计行、段落、region、或者整个缓冲区。到覆盖范围该多大?Emacs的鼓励使用弹性的方式。可以将函数设计为处理region。这样即使需要统计整个缓冲区,也可以先用C-x h(mark-whole-buffer)先选定整个缓冲区。 统计字数是一个重复的动作:从region的开始位置,开始统计第一个词,然后是第二个,然后第三个,如此继续直到缓冲区的结束位置。这意味着单词统计的工作适合于使用递归或者while循环。 设计count-words-region函数首先,我们将使用while循环实现单词统计,然后是递归。当然,这个命令需要交互。 交互式函数定义如下: (defun name-of-function (argument-list) 我们所要做的就是填空。 函数名应该是自描述的与已存在的count-lines-region类似。这可以让命令名容易被记住。count-words-region是一个较好的名称。 这个函数统计region中的字数。这说明参数列表中需要两个符号,分别绑定到region的开始位置和结束位置。这两个位置可以被称为 beginning和end。文档字符串的第一行必须是一个完整的句子,因为有些命令将只打印文档的第一行,比如apropos命令。交互式语句 (interactive "r")将把缓冲区开始位置和结束位置放到参数列表中。 函数体需要完成三个任务:第一,设置条件,在这个条件下while循环可以统计字数。第二,执行while循环。第三,向用户显示信息。 当用户调用count-words-region时point可能位于region的开始位置或结束位置。但是,计数处理只能从region的开始 位置到结束位置计数。这意味着如果point没有在region的开始位置,则我们需要将point设置到region的开始位置,执行(goto- char beginning)。为了保证在函数执行完后,point可以恢复原来的位置,将需要用到save-excursion语句。 函数体的中心部分是由一个while循环组成,它内部有一个每次向前跳转一个单词的语句,另一个语句负责计数。while语句的true-or-false-test应该在point达到region结束位置时返回false,在此之前返回true。 我们可以使用(forward-word 1)作为向前移动一个单词的语句,如果我们使用正则表达式搜索就很容易明白Emacs中对于'word'的界定。 通过一个正则表达式查找到那个位置并把point设置在最后一个字符的后面。这表示成功的向前移了一个单词。 实际上还有一个问题,我们需要这个正则表达式跳过单词间的空格和标点符号。这表明正则表达式需要能匹配单词后面的空白和标点符号。(一个单词后面也可能没有空白和标点,因此正则表达式的这一部分应该是可选的) 因此,我们需要的正则表达式,要能匹配一个或多个构词字符(能构成单词的字符),后面跟一个可选的由一个或多个非构词字符(不能用于构成单词的字符)。正则表达式如下: \w+\W* 缓冲区的语法表决定了哪些是构词字符。 查找语句如下: (re-search-forward "\\w+\\W*") (注意w和W前面的双斜线。单个斜线对于Emacs Lisp解释器来说有特殊意义。它表明后面一个字符需要不同的处理。比如, 我们还需要一个计数器用于计数;这个变量初始时必须为0,然后在每次执行while循环体时增加。这个语句如下: (setq count (1+ count)) 最后我们需要告诉用户region中有多少个字符。message函数用于向用户显示信息。显示信息只需要一个短语,我们并不需要很复杂。到底是简 单还是复杂。我们可以用一个条件语句来解决定个问题。共有三种可能:region中没有单词,region只有一个单词,或者有多个单词。这时crond 比较合适。 初步的函数定义如下: ;;; First version; has bugs!这个函数能够工作,但并不是在所有的情况下。 count-words-region函数中空白处理的Bug前面描述的count-words-region命令有两个Bug,或者说一个Bug的两个表现。首先,如果 region中只在某些文本间有空白,count-words-region命令将告诉你region中只包含了一个单词。第二,如果region中只有 缓冲区结束位置或者narrowed缓冲区的可访问域的结束位置有空白,命令在执行时将显示错误信息: Search failed: "\\w+\\W*" 可以在Emacs中先安装这个函数,然后将它绑定到按键上: (global-set-key "\C-c=" 'count-words-region)可以在设置region后按 C-c = 执行(如果没有绑定按键,可以用M-x count-words-region执行)。
对下面的内容执行时Emacs将告诉你,region有3个单词。 one two three 如果把mark设置在这行的开头位置,point放在 第三个测试,复制上面例的整行到*scratch*缓冲区中并在行的结束位置输入一些空格。将mark设置在单词 这两个bug来自于同一个问题。 思考这个Bug的第一个表现,命令告诉你行的开始位置的空白包含一个单词。它是这样产生的:count-words-region命令先将 point移到region的开始位置。然后测试当前point的位置是否小于end变量的值。结果为true。接下来,通过表达式查找第一个单词。它将 point设置在第一个单词的后面。count被设置为1。while循环重复,但这时point已经大于end的值了,循环退出;函数显示信息说在 region中有一个单词。简单来说就是由于正则表达式查询时,它查找到的单词的结束位置超过了region的区域。 Bug的第二个表现中,region是缓冲区结束位置的空白。Emacs说Search failed。这是由于在while的true-or-false-test返回true,search语句被执行。但是由于没找到匹配项,因此查询失败。 这两种情况都是由于查询时扩展或者试图扩展到region的外部。 解决办法就是限制查询的区域,一个很简单的动作,但并没有想像的那么简单。 前面在讲re-search-forward函数时,它接收四个参数。第一个参数是必需的,其它三个是可选参数。它的第二个参数是用于限定查询范围 的。第三个可选参数,如果为t,则函数将在查询失败时返回nil,而不显示错误信息。第四个可选参数是重复次数。(可以用C-h f查找函数的文档) 在count-words-region函数定义中,region的结束位置被以设置到end参数上,它将作为函数参数传入。因此我们可以把end作为正则表达式查询时的参数。 (re-search-forward "\\w+\\W*" end)如果只对count-words-region的定义作上面的修改,在遇到一些空白字符时,仍将得到Search failed的错误。 这是因为,有可能在限制的范围内,搜索不到构词字符。搜索将失败,并显示错误信息。但我们在这时并不想要获取错误信息,我们需要显示"The region does NOT have any words."。 解决这一问题的办法就是将re-search-forward的第三个参数设置为t,这样在函数在搜索失败时将返回nil。 如果你尝试运行程序,你将看到信息"Couting words in region..."并一直看到这条消息,直到你输入C-g(keyboard-quit)。 当在限制查询范围的region中搜索时,和前面一样,如果region中没有构词字符,搜索将失败。re-search-forward语句返回 nil。这时point也不会被移动,而循环中的下一条语句将被执行。这条语句将计数增加。然后循环继续。true-or-false-test将一直返 回true,因为point仍小于end参数,程序将陷入死循环。 count-words-region的定义还需要一些修改,以便在搜索失败时让true-or-false-test返回false。可以在 true-or-false-test中增加一个条件,true-or-false-test在增加计数前需要满足下面的条件:point必须在 region之内,且查询的语句必须找到了一个单词。 因为两个条件都必须为true。所以区域范围检查和搜索语句可以用and连接起来,都作为while循环的true-or-false-test: (and (< (point) end) (re-search-forward "\\w+\\W*" end t))re-search-forward在成功搜索到单词后将返回t,并移动point,只要能找到单词,point将继续移动。当搜索失败或者point达 到region的结束位置时,true-or-false-test将返回false。while循环退出,count-words-region函数显 示一个或多个信息。 修改完后的count-words-region函数如下: ;;; Final version: while 递归方式统计单词数量上一节已经编写过了通过while循环进行计数的函数。 在这个函数中,count-words-region函数完成了三个工作:为计数设置适当的条件;计算region中的字数;将字数显示给用户。 如果我们在一个递归函数中执行所有的操作,则我们将在每次递归调用时都会得到字数的消息。如果region中包含了13个单词,消息将显示13次。 这并不是我们需要的,我们需要写两个函数来做这个工作,一个函数(递归函数)将在另一个函数内部被使用。一个设置条件和显示信息,国一个返回字数。 开始编写函数。我们仍把这个函数叫作count-words-region。 根据前一个版本,我们可以描述出这个程序的结构: ;; Recursive version; uses regular expression search定义很直接,不同的地方是递返回的数字必须传递给message来显示。这可以用let语句来完成:我们可以用let语句把字数赋给一个变量,并把这个值作为递归部分的返回值。使用cond语句,用于设置变量和显示信息给用户。 通常let语句总被作为函数的'次要工作'。但在这里,let将作为函数的主要工作,统计字数的工作就是在let语句中。 使用let时函数定义如下: (defun count-words-region (beginning end) 接下来我们需要编写递归计数函数。 递归函数至少有三个部分:'do-again-test','next-step-expresssion'和递归调用。 do-again-test决定函数是否继续调用。因为我们在统计region中的单词时我们使用了移动point的函数,do-again- test可以检查point是否位于region中。do-again-test需要检查point是位于region结束位置的前面还是后面。我们可以 使用point函数获取point的位置信息,我们还需要传递将region的结束位置作为参数传递到递归计数函数里。 另外,do-again-test还需要检查是否找到了一个单词。如果没有,函数就不再需要继续调用它自己了。 next-step-expression修改某个值以便递归函数能在适当的时候停止递归调用。在这里next-step-expression可以是移动point的语句。 递归函数的第三个部分是递归调用。 在这个函数中我们也需要在某个地方执行计数工作。 这样,我们有了一个递归计数函数的原型: (defun recursive-count-words (region-end)现在我们需要填空。首先我们从最简单的一种情况开始:point位于region结束位置或位于region之外,region中没有单词,因此函数需要返回0。同样,如果搜索失败,函数也需要返回0。 另一方面,如果point在region内部,并且搜索成功,函数应该再次调用它自己。 这样,do-again-test应该如下: (and (< (point) region-end)注意,查找语句是do-again-test函数的一部分,在搜索成功时返回t,失败时返回nil。 do-again-test是if语句的true-or-false子句。如果do-again-test成功,则if语句的then部分执行,如果失败,则应该返回0,因为不管point是位于region的外面还是搜索失败都表示region中没有单词。 另外,do-again-test返回t或nil时,re-search-forward将在搜索成功时移动point。这是修改point的值并 让递归函数在point移出region后停止递归调用的操作。因此,re-earch-foreard语句就是next-step- expression。 recursive-count-words函数如下: (if do-again-test-and-next-step-combined 怎样加入计数机制呢? 我们知道计数机制应该与递归调用联合起来。由于next-step-expression将point一个个单词的移动,因此,针对每个单词都会调用一次递归函数,计数机制必须有一个语句将recursive-count-words的返回值加1。 思考下面几种情况:
从上面的描述中可以看出if语句的else部分在没有单词时返回0。而if语句的then部分必须返回1加上region中其它单词数量的值。 语句如下,使用了函数1+使它的参数加1。 (1+ (recursive-count-words region-end))整个recursive-count-words函数如下: (defun recursive-count-words (region-end) 研究一下它是如何工作的: 当region中没有单词时,if语句的else部分被执行,函数返回0。 如果region中有一个单词,point的值小于region-end并且搜索成功。这时,if语句的true-or-false-test为true,if语句的then部分被执行。计数语句被执行。这个语句将返回(整个函数的返回值)递归调用的返回值加1的结果。 与此同时,next-step-expression将使point跳过region中的第一个单词。这表示当(recursive-count- words region-end)在第二次时被执行,并作为递归调用的结果,point的值将等于或大于region的结束位置。这样,recursive- count-words将返回0。最初的recursive-count-words将返回0+1,计数正确。 如果region中有两个单词,第一次调用recursive-count-words将返回1加上在包含其它单词的region上调用recursive-count-words的返回值,这里将是1加1,2是正确的返回值。 类似地,如果region中包含有3个单词,第一次调用recursive-count-words将返回1加上在包含其它单词的region上调用recursive-count-words的返回值,如此继继续。 整个程序包含了两个函数: 递归函数: (defun recursive-count-words (region-end) 包装函数: ;;; Recursive version 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
浏览 4355 次