nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

format Justification (幅揃え)

Common Lispformatの命令である、Justification ~<...~>の説明をします。

この機能はただ空白を均等に出力するだけなのですが、 習得するにはわりと困難だと思います。
理由は「いや、こんな機能どうせ使わないし」と 思って気分が乗らないからです。
だって本当に使わないし。

まあそれは置いておいたとして、機能そのものは何も難しいことはありません。
パッと見れば何をしているのか理解できます。
例えば下記の通り。

(format t "~30<AAA~;BBB~;CCC~>~%")
AAA           BBB          CCC

例では、~30<~>で囲まれた箇所を~;で3つに分けています。
その分かれた部分を、30文字に収まるように空白を均等に分配します。
それだけです。

じゃあ何が難しいのか。
一つはLogical Blockという全く関係のない機能が同居しているということ。
他は細かくてどうでもよさそうな機能がいくつもあるためです。

実際にJustificationではなくPretty Printingを使った方がいい場合があります。
しかしPretty Printingを使えと言われても、 たぶんほとんどの人が、そんな訳わからん機能なんて使えないよと 諦めるんじゃないでしょうか。

覚えなくても生きていける、だからこそ誰も覚えない。
そんな理屈がJustification(正当化)されてしまう、 これこそが真のJustificationです。

本投稿の目的は、不遇のJustificationを一つずつ例文として示すことです。

Logical Blockはまた今度

Logical Blockとは、Pretty Printingのpprint-logical-blockのことです。
Justificationは~<...~>で囲んだものですが、 Logical Blockは終わりのカッコに:を付けた、~<...~:>で囲んだものです。

両者はほとんど関係のない機能です。
Logical Blockの説明は、また別の機会にしようかと思います。

Justificationの使い方

では例をあげて行きます。

まずは単純な例

第一引数で幅30を指定します。

(format t "++~30<AAA~;BBB~;CCC~>++~%")
++AAA           BBB          CCC++

++++の間が幅30に収まるように均等に空白を分配しているのがわかります。

空白量の確認

各節の長さ(AABBBBBBBBBの文字数のこと)が違っていても空白の量は同じです。

(format t "++~30<AA~;BBBBBBBBB~;C~;DD~;EEE~>++~%")
++AA    BBBBBBBBB   C   DD   EEE++

:@

:@で、前後に空白を追加できます。
まずは:にて前に空白を挿入します。

(format t "++~30:<AAA~;BBB~;CCC~>++~%")
++       AAA       BBB       CCC++

@にて後ろに空白を挿入します。

(format t "++~30@<AAA~;BBB~;CCC~>++~%")
++AAA       BBB       CCC       ++

:@で前後に空白を挿入します。

(format t "++~30:@<AAA~;BBB~;CCC~>++~%")
++      AAA     BBB     CCC     ++

要素が一つの場合

要素が一つの場合は右揃えです。

(format t "++~30<AAA~>++~%")
++                           AAA++

右揃えなので、:は意味がありません。

(format t "++~30:<AAA~>++~%")
++                           AAA++

@を指定すると左揃えにになります。

(format t "++~30@<AAA~>++~%")
++AAA                           ++

:@の両方を指定すると中央揃えです。

(format t "++~30:@<AAA~>++~%")
++              AAA             ++

幅の指定

例文で示して来たとおり、第一引数は幅の大きさです。
例えば次の通り。

(format t "++~40<AAA~;BBB~;CCC~>++~%")
++AAA                BBB               CCC++

第一引数を省略すると0が指定されたことになります。

(format t "++~<AAA~;BBB~;CCC~>++~%")
++AAABBBCCC++

幅0に対して、AAABBBCCCの9文字を出力するのは無理なので、 そのまま出力されています。

幅の拡張

もし第一引数の幅に収まらなかった場合は、幅を拡張します。
拡張する量は、第二引数により指定できます。
省略時は1です。

(format t "++~,20<AAA~;BBB~;CCC~>++~%")
++AAA      BBB     CCC++

この例では、幅0に収まらなかったので、 幅20で試してみて、収まるようだったので その内容を出力しています。

(format t "++~5,20<AAA~;BBB~;CCC~>++~%")
++AAA        BBB        CCC++

この例では、幅5で試して見たのですが、 とても収まらなかったため20を足して幅25で出力しています。
もし幅25でダメだったら、さらに20を足して幅45で出力することになります。

最小の空白

第三引数にて、最小の空白の個数を指定できます。

(format t "++~,,5<AAA~;BBB~;CCC~>++~%")
++AAA     BBB     CCC++

省略された第一引数である幅0に収まらないので、 第三引数である最小の空白5文字とともに出力されています。

(format t "++~30,,5<AAA~;BBB~;CCC~>++~%")
++AAA           BBB          CCC++

この例では、幅30で空白を分配した結果、 空白量が最小値の5をクリアしているため、 第三引数を省略した結果と同じ内容が出力されています。

(format t "++~15<AAA~;BBB~;CCC~>++~%")
++AAA   BBB   CCC++

(format t "++~15,20,5<AAA~;BBB~;CCC~>++~%")
++AAA           BBB          CCC++

上の例は単純に幅15文字に収めた結果であり、空白は3つ出力されています。
下の例は、最低の空白量を5と指定していますが、 幅15だと空白は3つしかないため、 幅15から20を加算した幅35で再試行した結果を返却しています。

空白文字の指定

第四引数にて、空白文字を指定できます。
これはもう例文を見ると一発でわかります。

(format t "++~30,,,'*<AAA~;BBB~;CCC~>++~%")
++AAA***********BBB**********CCC++

中断

~^により中断することができます。
一応説明しておくと~^は引数がもうない場合に中断する命令です。

例えば次の通り。

(format t "++~30<AA~;BB~;CC~;DD~;~^EE~>++~%")
++AA        BB       CC       DD++

上記の例では、~^によりEEを無視して出力しています。

(format t "++~30<AA~;BB~;CC~;~^DD~;EE~>++~%")
++AA            BB            CC++

この例では、~^によりDD以降を全て無視して出力しています。
規約には~^~;の直後に指定するべきだとの記載があります。
なぜなら~^を指定するとその節は全て無視されるからです。
例を示します。

(format t "++~30<AA~;BB~;CC~;DD~^dd~;EE~>++~%")
++AA            BB            CC++

この例ではDD~^ddとなっていますが、 節の全てが無視されるので、ddだけではなく 最初のDDすら無視されているのがわかります。

画面の幅に合わせる

ここまでは~<の基本的な使い方を説明してきました。
ここからは「画面の幅」という考え方が登場します。

まずは下記の二例を見て行きましょう。

(format t "++~30<AAA~;BBB~;CCC~>++~%")
++AAA           BBB          CCC++

(format t "++~30<AAA~:;BBB~;CCC~>++~%")
++BBB                        CCC++

両者の違いは、AAA~;~:;のどちらで終わらせているかの違いになります。
上の例は今まで説明してきたものですが、 下の例はまた別の方法で処理されるため、出力が違っています。

~<...~>では、最初の節だけ:が付いた^:;で終わらせることができます。
その場合に限り「画面の幅」というものが考慮されます。

出力結果が変わっていますが、一体どういう事なのでしょうか?
動作としては、まずはAAA, BBB, CCCformatの処理を進めていくのですが、 最初のAAAは特別な場合に出力されるものとして保留しておき、 BBBCCCの2つの節に対していつもの~<...~>処理を行います。

幅揃えとしては最初の節であるAAAは含まれません。
30文字に収まるかどうかの判定は、AAAを除外したBBBCCCでのみ実施されるのです。
AAAは何なのかというと、もしBBBCCCの整形結果が画面の幅を超えていた場合は 最初にAAAがそのまま出力されます。

では画面の幅とは何なのでしょうか?
Pretty Printingだと*print-right-margin*でした。
でもJustificationはそんな変数を使いません。

画面の幅とは出力するstreamに問い合わせを行い、 もしstreamが端末に関係するならその幅を返却するということになるのかと思います。

完全に処理系依存ですが、例えば*terminal-io*とか、 リダイレクトされていない*standard-output*に出力するなら、 ioctl関数なんかでwindowサイズを調査するんじゃないでしょうか。
Pretty Printingの*print-right-margin*nilだった場合は そんな感じで幅を返却しています。

そんなふうに色々やってみても、 もしファイルへの出力だった場合には画面のサイズなんてありません。
画面の幅が分からなかったときは、72を使うんだそうです。
72は規約に書いてある定数です。

うちの端末は72なんかより大きいよ!
なんて思っても、たぶんどうにもならないと思うのですが、 一応は最初の節を終端させた~:;の第二引数で画面幅を指定できます。
例えばこんな感じ。

(format t "++~30<AAA~,10:;BBB~;CCC~>++~%")
++AAABBB                        CCC++

この例では、~,10:;により画面の幅を10に指定しています。
最初に++を出力していますので、その2文字を考慮し、 2文字+30文字=32文字が画面の幅である10を余裕で越えているため、 最初の節のAAAが出力されているのが分かります。

最初の++の2文字を考慮しているということは、 改行からの文字数をちゃんとカウントしているわけです。
まるでPretty Printingですね!

実際にそんな動作を期待して設計された機能のようです。
例えば規約に載っている次の例文を考えます。

"~%;; ~{~<~%;; ~1,50:; ~S~>~^,~}.~%"

上記の例にある~1,50:;の第一引数はあとで説明します。
実行例を次に示します。

(format t "~%;; ~{~<~%;; ~1,50:; ~S~>~^,~}.~%"
  '(100000000 2000000000 hellohellohello 4000000 599999999))

出力結果は下記の通り。

;;  100000000, 2000000000, HELLOHELLOHELLO,
;;  4000000, 599999999.

かなりわかりづらいのですが、 最初の節が~%;;であり、 画面50文字指定で~S,を出力していると考えればいいと思います。
もし画面50文字を超えた場合は、改行と;;が出力されるというもの。

Pretty Printingを使って書き換えるとこんな感じ。

(setq *print-pretty* t)
(setq *print-miser-width* nil)
(setq *print-right-margin* 50)

(defun output (list)
  (pprint-logical-block (nil list :per-line-prefix ";;  ")
    (loop (write (pprint-pop))
          (pprint-exit-if-list-exhausted)
          (write-string ", ")
          (pprint-newline :fill)))
  (fresh-line))

(output '(100000000 2000000000 hellohellohello 4000000 599999999))

出力結果は下記の通り。

;;  100000000, 2000000000, HELLOHELLOHELLO,
;;  4000000, 599999999

そのうち説明する予定ですが、formatのLogical Blockを使うと 次のように書くこともできます。

(setq *print-pretty* t)
(setq *print-miser-width* nil)
(setq *print-right-margin* 50)

(format t "~<;;  ~@;~@{~W~^, ~:_~}~:>~&"
  '(100000000 2000000000 hellohellohello 4000000 599999999))

出力結果は下記の通り。

;;  100000000, 2000000000, HELLOHELLOHELLO,
;;  4000000, 599999999

Pretty Printingは意味不明過ぎるけど、よく考えられていると思います。
以前の投稿である pprint-newlineの使い方 - nptclのブログ の説明を合わせて見ると 理解できるのではないでしょうか。

それでは続けて~:;の第一引数について説明します。
最初の節のAAAが出力されるには、 第一引数の量の余裕を持たせないといけないとのことです。
つまりBBBCCCの整形した文字数と現在位置だけではなく さらに第一引数の量を加算した結果が画面の幅と比較されます。
言い換えるなら、~1,50:;~,49:;は同じです。

規約の例である~1,50:;は、 出力されるカンマ,を考慮して第一引数に1を指定したとの記載があります。

こういうのを全部含めて考えると、画面の幅を考慮した出力は Pretty Printingを使った方が絶対いいですね。
覚えるまで大変ですが。

使用禁止の命令

Justificationは下記の命令が使えません。

~W          write
~_          pprint-newline
~I          pprint-indent
~:T         pprint-tab
~<...~:>    pprint-logical-block

つまり、Justificationの中では Pretty Printingに関わる全ての機能が使用不可になります。
writeも使えないのか。

よって次の命令はエラーになります。

(format t "~<~W~>~%" 100)

clispでは全く問題なく100が出力されますが、気にしないことにします。

最初の節での中断

二番目以降が中断された場合はすでに説明されていますが、 最初の節が中断された場合はどうなるでしょうか。

処理系によって動作がわかれました。
例えば次の通り。

(format t "++~30<A~^AA~;BBB~;CCC~>++~%")
sbcl, cclの結果
++++

clispの結果
++                              ++

画面の幅を考慮する~:;の場合も下記に示します。

(format t "++~30<A~^AA~:;BBB~;CCC~>++~%")
sbcl, cclの結果
++++

clispの結果
++                              ++

~:;の場合は、二番目の節を中断した場合も同じです。

(format t "++~30<AAA~:;B~^BB~;CCC~>++~%")
sbcl, cclの結果
++++

clispの結果
++                              ++

最初の節を中断した場合は、出力内容は期待しない方がよさそうですね。
個人的にはclispが正しいような気がします。