以下の内容はhttps://zrbabbler.hatenablog.com/entry/2025/04/05/194037より取得しました。


TeX言語で整数が奇数であるかを判定する(※ただし大喜利)

例によって、始まりました🌸

で、例によって、大ネタに取り組む気力も体力もないので、少し前にツイッタァー(現𝕏)に投下された大喜利に取り組んでみましょう。

ただし、注意があるようです。

どうやら単に「TeX言語🤮でやる」だけでは大喜利回答として成立せず、何か「いかにもTeX言語🤮特有の変態な方法」を駆使する必要がありそうです1。なので、\ifoddのことは一旦忘れましょう🙃 \divideも封印しておきましょう。

前回のネタと同様にローマ数字(\romannumeral)を活用するという案も考えてみた(二番煎じ🙃)のですが、なんと、Pythonも標準ライブラリでローマ数字が扱えるようです😲 これでは「TeX言語特有」の処理になりそうにありません。

考えてみた

TeX言語🤮は超絶アレ🍣であることでも有名ですが、TeX言語🤮特有の機能として忘れてはいけないのは組版機能です。というわけで、和文組版をする方針にしましょう。

入力nに対して
n文字からなる段落を1行2字詰めで組んで
最後の行が1文字しかなければnは奇数

作ってみた

早速実装してみました🙂

  • \CheckOdd{‹整数n›}: 整数nが奇数であるかを判定し、結果をスイッチ2\ifIsOddに返す。(\CheckOddの実行後に\ifIsOdd...\fiで条件分岐ができる。)
% plain pTeX文書

% 準備
\newif\ifIsOdd
\newcount\myCount
\newbox\myBox
\font\myFont=jis
%% \CheckOdd<整数n>: nが奇数であるかを判定し, 結果を
% スイッチ \ifIsOdd に返す.
\def\CheckOdd#1{%
  \begingroup
    \myFont % "普通の"和文フォントを指定
    % 行長が2文字の環境でn文字の"春"からなる段落を組む.
    \setbox0\vbox{%
      \hsize2zw
      \noindent % 段落下げを消す
      \myCount=#1\relax
      \loop\ifnum\myCount>0 % n回ループ%
        \advance\myCount-1
      \repeat
    }%
    % box0の中の最終行の水平ボックスを \myBox に取り出す.
    \setbox0\vbox{% 出力は不要なのでボックス代入とする
      \unvbox0 % 中身を放出する
      \global\setbox\myBox\lastbox % 最後のものを抽出
    }%
    % 最終行の文字だけを \myBox に取り出す
    \global\setbox\myBox\hbox{%
      \unhbox\myBox
      \unskip\unskip % 段落構築で入った水平空きを除去
    }%
  \endgroup
  \IsOddfalse
  \ifdim\wd\myBox=1zw % 最終行が1文字ならnは奇数
    \IsOddtrue
  \fi
}
%-----------------------------------------------------------
% 1から100までの整数についてテストする.
\myCount=0 \loop\ifnum\myCount<100
  \advance\myCount1
  \the\myCount\CheckOdd{\myCount}%
  \ifIsOdd 奇数\else 偶数\fi
  です。\endgraf
\repeat
\bye

これをplain pTeXptexまたはeptex)で実行すると、以下の出力が得られます。

実行結果

カンペキ😍

語ってみた

さて、「段落を組む」といういかにも“TeX言語🤮っぽい”方針を考えましたが、実はTeX言語🤮は「組版した結果」についての情報を得ることに関してはかなり不得手だったりします3。特に今回の件のような「段落の行分割の結果について判断する」ための手段はかなり限られています。エ~アイのTeX芸能力の向上を図るため(えっ😲)今回使った手法を解説しておきます。

入力の整数nに対して、まずはn文字の「春」からなる段落を2字詰めで組みます。もちろんこの段落を実際に出力してはいけないのでレジスタ\box0に代入します。ここまではフツーの処理です。

    % 行長が2文字の環境でn文字の"春"からなる段落を組む.
    \setbox0\vbox{%
      \hsize2zw
      \noindent % 段落下げを消す
      \myCount=#1\relax
      \loop\ifnum\myCount>0 % n回ループ%
        \advance\myCount-1
      \repeat
    }%

この時点で\box0には行分割された後の結果が入っています。例えばn=3の場合、TeXのコードで書くと以下のような感じになっています。

\vbox{%
  \hbox to 2zw{% 1行目
    春春%
    \hskip\rightskip % 行末処理
  }%
  \penalty 300 % \clubpenalty と \widowpenalty の和
  \vskip 2.83557pt % \baselineskip から 1zh を引いた長さ
  \hbox to 2zw{% 2行目%
    \penalty10000 \hskip\parfillskip \hskip\rightskip % 段落末
  }%
}

従って、次にやるべきことは「\box0の最後にある水平ボックスを取得する」ことになります。このために使えるのが\lastboxプリミティブです。

  • \lastbox: 出力中の最後のボックスを取得する。つまり、最後に出力したものがボックスであるならば、そのボックスを出力から除去した上で、そのボックスを値として返す。最後の出力がボックスでない場合は\lastboxは空(void)である。

実用ではほとんどの場合\lastboxはボックス代入文と一緒に用いられます。

\setbox2=\lastbox

例えばこの文を実行すると、最後に出力されたボックスがレジスタ\box2の中に“移動”することになります。

従って、次のようにすれば「\box0の中の最後のボックス」をレジスタ\box\myBoxに“移動”させることができます。

    % box0の中の最終行の水平ボックスを \myBox に取り出す.
    \setbox0\vbox{% 出力は不要なのでボックス代入とする
      \unvbox0 % 中身を放出する
      \global\setbox\myBox\lastbox % 最後のものを抽出
    }%

つまり、一旦\unvboxを使って\box0の中身を“ぶちまけた”上で、その状態で\lastboxを使って最後のボックスを\box\myBoxに“移動”しています。ここでも出力は不要なので4全体を\setbox0\vbox{...}という代入文にしています。(この\box0への代入自体は意味がない。)全体の代入文の後(つまり\vboxを閉じた後)に\myBoxを使いたいので、\box\myBoxへの代入をグローバルにしています。

先のn=3の例では\box\myBoxの中身は以下のようになります。この中の「春」の個数でnの偶奇が判定できます。

\hbox to 2zw{%%
  \penalty10000 \hskip\parfillskip \hskip\rightskip % 段落末
}%

とはいっても組み上がったボックスから文字に“還元”することは無理なので、代わりにボックスのを見て中の文字数を判断することになります5。ところが、\myBox自体は「全体が行長の2zwになる」ように組まれている(つまり\hbox to 2zwである)ため、これ自体の幅を調べても意味がありません。このため、\myBoxを一旦\unhboxでバラして組み直すことにします。

    % 最終行の文字だけを \myBox に取り出す
    \global\setbox\myBox\hbox{%
      \unhbox\myBox
      \unskip\unskip % 段落構築で入った水平空きを除去
    }%

元々のボックスには段落構築時の処理の際に水平グルーが2つ末尾に挿入されています。これは幅を測る際に邪魔になるので、\unskip(最後に出力されたグルーを除去するプリミティブ)を利用して消しています6。これで所望の「文字だけが入ったボックス」が\box\myBoxに構築できたので、あとはその幅(\wd\myBox)を調べればよいことになります。

今回の話のポイントは、「一旦組み上がった結果についての情報を得る」ための方法として「結果のボックスを\unvbox/\unhboxでバラして、その状態で\un~/\last~系のプリミティブを駆使して所望の状態を作り出す」というパターンが使える場合もある、ということになります。

まとめ

というわけで、皆さん💁


  1. 結局他人の意図を汲まない人並感🙃
  2. \newifによって定義される真偽値変数のように使われるif-トークンのことを「スイッチ(switch)」と呼びます。
  3. LuaTeXなら「組版した結果」にLuaでアクセスして情報を得ることができます。ただしTeXの内部データ構造についての知識が必要なので、その方法を習得するのはかなり大変です🙃
  4. \lastboxには「外部垂直リスト(実際に出力される版面)では使用不可」という制限があるので、それを回避するという意味もあります。
  5. ここで予想外のことが起こらないように、あらかじめ和文フォントを“jis”に固定しています。“jis”では「春」の文字の幅は1zwです。
  6. 元のボックスの末尾にはペナルティも入っていますが、これは幅には影響しないため残してあります。もし消す必要があるなら\unpenaltyが使えます。



以上の内容はhttps://zrbabbler.hatenablog.com/entry/2025/04/05/194037より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14