Emacs Lisp によるプログラミング -- 数え上げ:繰り返しと正規表現 --

昔どっかで見つけてきて印刷した1.0.4版の日本語訳や,
Emacs電子書棚さんにある1.0.5版の翻訳
ここにあるPDF版(1.0.5版)を参考にまたやっていこうと思う.
また,実行環境は,Meadow(バージョンはたぶん,3.0)の*scratch*バッファ上で行う.


まだついていけてる.
本当は練習問題もやったほうがいいんだろうけど,省いてしまう.いかんなあ.

関数count-words-region

リージョン内の単語数を数えるコマンド(を作成するのが今回).
リージョンの始めから始めて,最後まで単語を数え続ける.
再帰やwhileループを使用.

  • 関数は,リージョン内の単語を数える.
  • 引数リストには,リージョンの先頭と最後の位置にバインドされるシンボルが必要(begin, end).
  • 関数の本体の3つの仕事.
    • whileループが単語を数えられるように条件を設定する.
    • ループを走らせること.
    • ループ実行後,ユーザにメッセージを送る.
  • 数え上げの作業は,リージョンの先頭から行う.つまり,ポイントが移動するので,save-excursionを使う.
  • whileループは,ポイントが移動できる限り真となり,リージョンの最後に達したら偽となるようにする.
  • 単語移動には,(forward-word 1)と正規表現による移動が使える.



正規表現は,単語自体だけでなく,単語と単語の間の空白や句読点も飛ばすようにする.
空白や句読点を飛ばさないと,それにぶち当たったときに,操作が止まってしまう.
よって,正規表現は,単語を構成する1個以上の文字と単語を構成しない文字0個以上.

;; 正規表現
\w+\W*

;; 探索式
(re-search-forward "\\w+\\W*")  ;; 単独のバックスラッシュは,Emacsでは別の意味を持つため,2つ必要.

;; 単語を数えるカウンタ
(setq count (1+ count))

これらより,バグが残る第1版.

;;; 第1版。バグあり!
(defun count-words-region (beginning end)  
  "Print number of words in the region.
  Words are defined as at least one word-constituent
  character followed by at least one character that
  is not a word-constituent.  The buffer's syntax
  table determines which characters these are."
  (interactive "r")
  (message "Counting words in region ... ")

  ;; 1. 適切な条件を設定する
  (save-excursion
    (goto-char beginning)
    (let ((count 0))

      ;; 2.  while ループ
      (while (< (point) end)
        (re-search-forward "\\w+\\W*")
        (setq count (1+ count)))

      ;; 3. ユーザーにメッセージを与える
      (cond ((zerop count)
             (message
              "The region does NOT have any words."))
            ((= 1 count)
             (message
              "The region has 1 word."))
            (t
             (message
              "The region has %d words." count))))))
count-words-regionの空白に関するバグ

2通りのバグがある.

  • リージョンが文章の途中にある空白のみの場合でも,単語が1個あると報告する.
  • ナロイングしたバッファやバッファの参照可能な最後の部分にある空白のみを含むリージョンの場合,エラーを報告する.

次のようにすれば,キーに割り当てられる.

;; C-c = に count-words-regionを割り当て.
(global-set-key "\C-c=" 'count-words-region)
  • 最初のバグは,空白しかないはずの行頭部分に1つの単語が含まれていると報告すること.
    • コマンドは,リージョンの最初の部分にポイントを移動する.
    • pointの値がendの値よりも低いか検査する.
    • これは正.よって,最初の単語を見つける動作を行う.
    • countに1をセットする.
    • whileループは,もう1度,pointの値を検査する.
    • これは偽.よって終了する.
    • つまり,リージョンの外に単語があるにもかかわらず,それを見つけてしまうことによるバグ.
  • 二番目のバグは,リージョンには最後の空白しか無い場合の起こっている.
    • whileループの真偽テスト.これは真となる.
    • 単語の検索が実行される.しかし,存在しないとのでエラーと報告される.

解決法は,探索をリージョンに制限すること.

;; 探索範囲をendまでに制限する.
;; しかし,これでも二番目のバグによるエラー報告はそのままとなる.
(re-search-forward "\\w+\\W*" end)

;; 探索に失敗したらエラーの代わりにnilを報告するようにする.
;; これでも二番目のバグは,ループに入ってしまうので,逆にループから抜けられなくなる.
(re-search-forward "\\w+\\W*" end t)

;; ポイントがリージョンの最後に到達したり,単語を探すのに失敗したらループを抜ける.
(and (< (point) end) (re-search-forward "\\w+\\W*" end t))

;; 最終版 while
(defun count-words-region (beginning end)
  "Print number of words in the region."
  (interactive "r")
  (message "Counting words in region ... ")

  ;; 1. 適切な条件を設定する
  (save-excursion
    (let ((count 0))
      (goto-char beginning)

      ;; 2.  while ループ
      (while (and (< (point) end)
                  (re-search-forward "\\w+\\W*" end t))
        (setq count (1+ count)))

      ;; 3. ユーザーにメッセージを与える
      (cond ((zerop count)
             (message
              "The region does NOT have any words."))
            ((= 1 count)
             (message
              "The region has 1 word."))
            (t
             (message
              "The region has %d words." count))))))

再帰による単語の数え上げ

2つの関数に分ける.

  • 数え上げた単語数を返す関数.
  • 条件を設定してメッセージを表示する関数.
;; メッセージを表示する関数
(defun count-words-region (beginning end)
  "Print number of words in the region."
  (interactive "r")

  ;; 1. 適切な条件を設定する
  (message "Counting words in region ... ")
  (save-excursion
    (goto-char beginning)

    ;; 2. 単語を数える
    (let ((count (recursive-count-words end)))

      ;; 3. ユーザーにメッセージを与える
      (cond ((zerop count)
             (message
              "The region does NOT have any words."))
            ((= 1 count)
             (message
              "The region has 1 word."))
            (t
             (message
              "The region has %d words." count))))))

;; 単語を数える関数
(defun recursive-count-words (region-end)
  "説明文..."
  ;; 1. 再帰条件
  (if (and (< (point) region-end)
           (re-search-forward "\\w+\\W*" region-end t))

      ;; 2. 真の場合の動作:再帰呼び出し
      (1+ (recursive-count-words region-end))

    ;; 3. 偽の場合の動作
    0))