TeX Alchemist Online

TeX のこと,フォントのこと,Mac のこと

括弧位置が自動調整される \left, \right もどきを作る

この記事は TeX & LaTeX Advent Calendar 2021 の(勝手にやった)26日目の記事です。25日目は golden_lucky さんでした。その後なんと27日目28日目29日目30日目31日目と続きました。

昨日,TeX & LaTeX Advent Calendar 2021 が無事に完走しましたね。golden_lucky さんによる24~25日目のTeX上でのプログラミングに関する話がとても面白かった(TeXマクロを書くときの自分の心理の動きが見透かされたように明文化されていて刺さりました)ので,自分もTeX言語でのプログラミングの一例を挙げてみることにしました。

zenn.dev

zenn.dev

先日 TeX Q&A で「絶対値記号の高さの調整について」というトピックが立っていました。要するに,\left\right によって括弧を付けると上下対称に括弧が伸びて間延びする場合があるので,「括弧の中央が数式の中央ラインからずれてもよいから必要最低限の長さの括弧で囲ってほしい」という要望です。

\left\right を使った場合の組版結果】

f:id:doraTeX:20211226182659p:plain

【要望された組版結果】 f:id:doraTeX:20211226182910p:plain

このような組版が数式組版として適切かという議論はさておき,TeX言語プログラミングの練習として,このような組版をマクロで自動的に実現するための方法を考えてみましょう。

絶対値記号をこのように出力するための考え方とコードが,しっぽ愛好家さんの投稿に示されています。「絶対値の中身を一度ボックスに保存しておき,それを縦方向中央に移動させ,それを \left| ~ \right| で上下対称に囲み,再び移動量の分だけ元の高さに戻す,というわけです。

ここに提示されたマクロは,\norm{中身} という形で「中身」を絶対値記号で囲む命令となっていますが,これを

  • 絶対値に限らず様々な括弧類に汎用的に対応させる
  • \left\right と同様の構文で使えるようにする

ように拡張してみましょう。

ここでは,\autoleft\autoright という名前で,\left\right と同様の構文で使えるような命令を用意してみます。

コード例

\documentclass{article}
\usepackage{amsmath}
\usepackage{xstring}
\makeatletter

\newcount\autobracket@nest
\def\autoleft#1{%
  \autobracket@nest=\z@
  \def\autobracket@l{#1}%
  \def\autobracket@content{}%
  \autobracket@content@save
}

\def\autobracket@content@save#1\autoright#2{%
  %%%% #1 の中にいくつ \autoleft, \autoright があるかをカウントして,その個数差の分だけ後でスキップする
  \edef\@tempa{\detokenize{#1}}%
  \edef\@tempb{\detokenize{\autoleft}}%
  \StrCount{\@tempa}{\@tempb}[\@tempc]%
  \advance\autobracket@nest\@tempc\relax
  \edef\@tempb{\detokenize{\autoright}}%
  \StrCount{\@tempa}{\@tempb}[\@tempc]%
  \advance\autobracket@nest-\@tempc\relax
  \ifnum\autobracket@nest=\z@ %% #1 に余分な \autoleft が含まれないときは終結
    \expandafter\def\expandafter\autobracket@content\expandafter{\autobracket@content#1}%
    \def\autobracket@content@save@next{\autobracket@autoright#2}%
  \else %% #1 に \autoleft が余分に含まれるときはカウンタ値を減らして前に進む
    \advance\autobracket@nest\m@ne
    \expandafter\def\expandafter\autobracket@content\expandafter{\autobracket@content#1\autoright#2}%
    \let\autobracket@content@save@next\autobracket@content@save
  \fi
  \autobracket@content@save@next
}

\def\autobracket@autoright#1{%
  \def\autobracket@r{#1}%
  \autobracket@save@counters
  {\mathpalette\autobracket@enclose{\autobracket@content}}%
}

\newbox\autobracket@body

\def\autobracket@enclose#1#2{%
  \hbox{%
    \m@th\autobracket@restore@counters
    \setbox\autobracket@body\hbox{$#1#2$}%% 中身を保存
    \dimen@ii\dp\autobracket@body%% 中身の底の位置を保存
    \setbox\autobracket@body\hbox{$\vcenter{\box\autobracket@body}$}%
    %% ↑中身の天地中央の位置が「数式の軸」のところになるよう置き直す
    \advance\dimen@ii-\dp\autobracket@body%% 置き直した際の中身の移動量を計算
    \lower\dimen@ii\hbox{$\left\autobracket@l\box\autobracket@body\right\autobracket@r$}%
    %% ↑括弧を付けた後,上下方向について元の位置に置き直す
  }%
}
\let\autobracket@restore@counters\relax
\def\autobracket@save@counters{%
  \def\@elt##1{\noexpand\@nameuse{c@##1}\number\@nameuse{c@##1}\relax}%
  \edef\autobracket@restore@counters{\cl@@ckpt}%
}
\makeatother

\begin{document}
%% テスト
\[
x + \autoleft|\overrightarrow{AB}\autoright|^2 
+ \autoleft|\overline{z^{n^2}}\autoright|^2
+ \autoleft(\dfrac{\dfrac{x+1}{x^2}}{x^2+x+1}\autoright)^n 
+ \autoleft[\dfrac{x^2+x+1}{\dfrac{x+1}{x}}\autoright]_0^1 
+ \autoleft\{\dfrac{\displaystyle\sum_{k=1}^n a_k}{n}\autoright\}_{n=1,2,\ldots}
\]
%% ネストするケースのテスト
\[
\autoleft|\dfrac{\dfrac{x}{2}}{x+1}+4\autoleft(\dfrac{\dfrac{x}{2}}{x+1}\autoright)\autoright|
+\autoleft|\dfrac{2\autoleft(e^x+\dfrac{x}{2}\autoright)}{x^2-x}\autoright|
\]

\end{document}

出力結果

f:id:doraTeX:20211226222239p:plain

実装のアイデア

アイデアとしては,

  1. \autoleft が現れたら \autoright までをパターンマッチで取得
  2. 左右の括弧の種類を \autobracket@l\autobracket@r として保存しておく
  3. 囲まれた内容を保存し,それをしっぽ愛好家さんの \norm と同様に定義された \autobracket@enclose の中で使用する

というものです。ただし,単純に \def\autoleft#1#2\autoright#3 といった形でパターンマッチさせてしまうと,ネストして使用している場合に対応できません。#2 の部分が最短でマッチされ,個数がつり合うところの \autoright がマッチしないからです。

そこで,上記コードでは,パターンマッチしたものを即座に採用するわけではなく,中身をチェックしています。\def\autobracket@content@save#1\autoright#2 において,#1 にマッチしたトークン列の中に \autoleft\autoright いうトークンが含まれる個数の差をカウントする*1ことで「現在ネストの何重目にいるか」を判断し,その個数だけ \autoright を超えて先の \autoright まで繰り返しマッチさせていきます。その過程でスキップしたトークン列を追記しながら保持しておき,トップレベルの \autoright が閉じた瞬間,\autobracket@enclose にその中身と括弧の種類を引き渡す,という処理を行っています。これが再帰的に実行されることで,ネストされた \autoleft\autoright に対応できる,というわけです。

「トークン列の中に \autoleft, \autoright というトークンが何個含まれるか」を数えるには,xstring パッケージ\StrCount というコマンドを用いています。#1\autoleft, \autoright をともに \detokenize\the-文字列化しておいた上で,含まれる個数をカウントしています(カテゴリーコードまで一致しなければ一致判定されない点に注意)。

……というわけで,解説を始めると,どうしてもgolden_luckyさんの25日目の記事にある

「そんなマイナーなものすっとばして簡単な例でマクロの書き方だけさくっと教えろよ、なんでTeXの解説はそうやってすぐに細かいこと言い出すんだよ」と思うのが自然ですよね。

でも、こういうのに言及しておかないと「ハマらない」マクロの書き方が説明できないんですよ。だから言及するわけだが、その結果やたらにスノッビーな解説になってしまう。

のような解説の典型例になってしまいましたね😓。実際,このマクロを書くときの自分は,まさにこの記事にある

  1. 引数に対するパターンマッチでやれそう
  2. しかし、単一のパターンじゃないので、1つのマクロではパターンを網羅できないぞ
  3. 下請けマクロで再帰処理する必要がある
  4. そのために終端のマークを用意しよう → とりあえず\@endcommaを定義する
  5. まずはandは忘れてコンマ区切りのパターンマッチだな
  6. \def\@comma#1,#2\@endcomma{...の定義を考えはじめる
  7. \@endcommaを見つけるまで再帰、という基本方針は上記の3と4の時点で思い描いているので、それを書こうとする
  8. このへんでようやく、「最後のA and Bをどうやってキャッチするか」を決めないといけないことを思い出す
  9. 最後がA and B\hogehogeみたいな形になってれば、「再帰したときに#2\hogehogeかどうか」を\ifxで調べてキャッチできそう
  10. そのとき用の下請けマクロを書き出す(\def\@and#1 and #2{...
  11. あとは\hogehogeも準備しないといけないけど、もう面倒だから\relaxでいいか

というのと同様の試行錯誤をしました。みんな同じような心理でTeX言語プログラミングしているんだなぁと知って,ちょっと安心した次第です。

というわけで,皆様,今年も大変有益な Advent Calendar,ありがとうございました!よいお年を!

*1:パターンマッチにおいてスキップされる,ブレース {} に囲まれた内側にバランスの取れた \autoleft と \autoright が存在することも想定して,個数の「差」をカウントしています。