Emacsは柔軟性を重視し、文字専用端末を含むあらゆる種類の端末で動作するように設計されているため、グラフはタイプライター記号のいずれかを用いて作成する必要があります。アスタリスクで十分でしょう。グラフ出力機能を拡充していく過程で、記号の選択をユーザーオプションにすることも可能です。
この関数を graph-body-print と呼びます。唯一の引数として numbers-list を受け取ります。現時点ではグラフにラベルは付けず、その本体のみを出力します。
graph-body-print 関数は、numbers-list の各要素に対してアスタリスクの縦列を挿入します。各行の高さは、numbers-list のその要素の値によって決定されます。
列の挿入は繰り返し処理となるため、この関数はwhileループを用いるか、あるいは再帰的に記述することができます。
最初の課題は、アスタリスクの列を出力する方法を見つけることです。通常、Emacsでは、文字を入力することで、画面上に1行ずつ水平に出力します。取り得る方法は2つあります。独自の列挿入関数を自作するか、あるいは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: 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. After this command, the mark is at the upper left corner and point is at the lower right corner.
期待通りに動作するか確認するため、簡単なテストを行ってみましょう。
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
に毎回同じ長さのリストを渡す方法が見つかれば、毎回同じ直線上に点を配置しつつ、新しい列ごとに右へ1列分ずらすことができます。ただし、この方法をとる場合、insert-rectangle
に渡されるリストの一部の要素は、アスタリスクではなく空白である必要があります。例えば、グラフの最大高さが5で、列の高さが3である場合、insert-rectangle には次のような形式の引数が必要になります:
(" " " " "*" "*" "*")
この最後の提案は、列の高さを決定できればそれほど難しくありません。列の高さを指定する方法は2つあります。任意に高さを指定する方法で、その高さのグラフであれば問題ありません。あるいは、数値のリストを検索し、そのリストの最大値をグラフの最大高さとして用いる方法です。もし後者の操作が困難であれば、前者の手順が最も簡単になりますが、Emacsには引数の最大値を求める組み込み関数があります。その関数を利用できます。この関数はmaxと呼ばれ、すべての引数(数値である必要があります)のうち最大のものを返します。したがって、例えば、
(max 3 4 6 5 7 3)
7を返します。 (min という対応する関数は、すべての引数のうち最小のものを返します。)
しかし、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
です。この関数は、第1引数(関数)を残りの引数に適用します。なお、残りの引数の最後はリストであっても構いません。
例えば、
(apply 'max 3 4 7 3 '(4 8 5))
8を返します。
(ちなみに、このような本なしに、どうすればこの関数について知ることができるのだろうか。search-forwardやinsert-rectangleといった他の関数は、名前の一部を推測してaproposを用いることで見つけ出すことも可能だ。比喩的な根拠――第1引数を残りの部分に適用する――は明確だが、初心者がaproposやその他の手がかりを使う際に、わざわざその特定の言葉を思いつくとは考えにくい。もちろん、私の考えが間違っているかもしれない。何しろ、この関数を最初に名付けたのは、それを考案せざるを得なかった人物なのだから。)
apply への2つ目以降の引数は省略可能であるため、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コマンドで挿入するための文字列リストを返す必要があります。
各列はアスタリスクまたは空白で構成されています。関数には列の高さと列内のアスタリスクの数が渡されるため、空白の数は、列の高さからアスタリスクの数を引くことで求められます。空白の数とアスタリスクの数が与えられた場合、2つのwhileループを用いてリストを構築できます:
;;; 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
に重大な欠陥があります。列内の空白およびマーク付き項目に使用される記号が、スペースとアスタリスクとしてハードコードされている点です。プロトタイプとしてはこれで問題ありませんが、あなたや他のユーザーが別の記号を使用したい場合もあるでしょう。例えば、グラフ関数のテストにおいて、insert-rectangle
関数が呼び出されるたびに点が適切に再配置されることを確認するため、スペースの代わりにピリオドを使用したい場合があるかもしれません。あるいは、アスタリスクを
‘+’
記号やその他の記号に置き換えたい場合もあるでしょう。さらに、グラフ列の幅を1つの表示列より広くしたい場合もあるかもしれません。プログラムはより柔軟であるべきです。それを実現するには、空白とアスタリスクを
graph-blank と graph-symbol と呼ぶ2つの変数に置き換え、それらの変数を個別に定義すればよいでしょう。
また、ドキュメントの記述も不十分です。こうした点を踏まえ、関数の第2版を以下のように作成しました。
(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.")
(defvarの解説については、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を3度目に書き換えることで、棒グラフだけでなく折れ線グラフも任意に提供できるようにできます。これはそれほど難しいことではありません。折れ線グラフの捉え方の一つは、各棒の上部より下の部分が空白になっているだけの棒グラフであるということです。折れ線グラフ用の列を作成するには、関数はまず値より1つ短い空白のリストを作成し、次にconsを用いてそのリストにグラフ記号を付加します。その後、再びconsを用いて上部の空白をリストに付加します。
このような関数をどう書けばよいかは容易に分かりますが、必要ないため、ここでは行いません。とはいえ、実現は可能ですし、もし実現するのであれば column-of-graph
で行えるでしょう。さらに重要なのは、他の箇所で変更を加える必要がほとんどないという点です。もし将来的に機能拡張を行いたいと考えたとしても、その手順は単純です。
さて、いよいよ最初の本格的なグラフ描画関数です。これは縦軸と横軸のラベルではなく、グラフの本体を描画するため、これをgraph-body-printと呼ぶことにします。