マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

Typstでの文字のsizeとサイズの話

この​「フォントサイズが実際にどう決まるのか」​という件について、もう少し詳しく説明する。

以下に述べる内容はTypstを実際に使う実験試行から導出したものである。公式の仕様ではないことに注意してほしい。

フォントサイズはどう扱われるか

まずはフォントサイズに関する基本的な事項を確認する。

  • Typstでのテキストのフォントサイズは「textエレメントのsizeパラメタ」(以降ではこれをtext.sizeと書く1ことにする)の値に一致する。
  • このtext.sizeパラメタはlength型の値をとる。
  • text.sizeパラメタの値を変更したい場合はset規則を用いる。
    例:set text(size: 20pt)
  • Typstの各種のエレメント(rawエレメント、math.equationエレメント、……)は種類ごとに異なる独立したtextエレメントのパラメタ」を保持していて、当然その中にはtext.sizeも含まれる。
  • rawエレメントのテキストのフォントサイズは「rawのtext.size」の値に一致する。
  • 「rawのtext.size」の値を変更したい場合はshow-set規則を用いる。
    例:show raw: set text(size: 20pt)
  • エレメント独自の方ではない本来のパラメタのことを本記事では「メインのパラメタ」と呼ぶことにする。

以上の事項を確認するための例を示す。

// Typstの既定のフォント設定だと"フォント自体の見かけの大きさ"が
// 不揃いであり紛らわしいので, 同じ系統のフォントに合わせる.
#set text(font: "Harano Aji Mincho")
#show raw: set text(font: "Harano Aji Gothic")

#set text(size: 14pt) // フォントサイズを設定
#show raw: set text(size: 14pt) // "rawのフォントサイズ"を設定
☃は素敵。// 14pt
`☃は素敵。`// 14pt

// text.sizeと"rawのtest.size"は独立して設定可能.
#set text(size: 10pt)
#show raw: set text(size: 20pt)
☃は素敵。// 10pt
`☃は素敵。`// 20pt

出力結果

em部分はどう扱われるか

Typstのlength型は、絶対的な長さ(abs)とフォントサイズ相対の長さ(em)からなる複合的な値2(例えば4pt+2em)である。length型の値を「実際の長さ」として解釈する場合にはem部分の値は「現在コンテキストのtext.sizeの値」として換算される(text.sizeが11ptであれば4pt+2emは26ptと解釈される)。一方で、set規則やshow-set規則でtext.sizeパラメタの値を設定する際に「現在コンテキスト」を参照するのは望ましくないだろう。

text.sizeの設定の際にはem部分は以下のように扱われる。

  • set規則やshow-set規則でtext.sizeに「Apt+Eem」を設定すると、実際には「『現在のtext.sizeの値』を E 倍して Apt を加算した値」に更新される。
  • 例えば「現在のtext.size」が8pt+2emであるときにset text(size: 5pt+0.5em)を実行すると、text.sizeの値は(8pt+2em)*0.5+5pt、つまり39pt+1emに更新される。
  • em部分が「現在のコンテキスト」に基づいて“解決”されるのではないことに注意。更新後の値も一般的にはem部分を持つ。
  • ただし、前述の規則から導かれる性質として、現在値がem部分を持たない場合は、どんな値を設定しても更新後の値は決してem部分を持たない。例えば現在値が11ptのときにset text(size: 5pt+0.5em)を実行すると10.5ptに更新される。
  • メインのtext.sizeの初期値は11ptである。従って先述の規則により、メインのtext.sizeは決してem部分を持たない。

text.sizeの値から「実際のフォントサイズ」を求める際にはem部分は以下のように扱われる。

  • メインのtext.sizeはem部分を持たないのでその扱いを考える必要はない。メインのtext.sizeは常に絶対的な長さでこれがそのまま「実際のフォントサイズ」となる。
  • rawのtext.sizeはem部分を持つ。これは当該のrawエレメントを含むすぐ外側のコンテキストの「実際のフォントサイズ」に換算される。
  • 例えば、rawのtext.size4pt+0.5emであり現在のフォントサイズが11ptである場合、rawエレメントの「実際のフォントサイズ」は9.5ptとなる。

text.sizeの初期値は以下の通りである。

  • メインのtext.size11pt
  • rawのtext.size0.8em

ここまで述べた規則を考慮すれば、メインとrawのフォントサイズを完全に把握することができるはずである。

実際にフォントサイズはどう扱われるか

さて今まで散々「規則」を述べてきたわけであるが、Typstの動作がそれと一致していなければ意味がない。そこで実際のTypstの動作を確認したいわけであるが、それには「現在のフォントサイズ」を調べる手段が必要である。

Typstの「現在のフォントサイズ」を調べる

Typstのコードモードにおいてtext.sizeという式を書くと現在のコンテキストにおけるtext.sizeの値が取得できるはずである。ただし実際の動作としてはこれで取得できる値はem部分を解決した後の「現在のフォントサイズ」のようである。ともかく、以下のマークアップで「現在のフォントサイズ」を知ることができる。

#context [#repr(text.size)]

ただしrawエレメントについてはその特質上「rawの中でコードモードに移行する」ことができないので、「現在のフォントサイズ」を知るには少しトリックが必要である。以下のようなコードを利用する。

#show raw: _ => repr(text.size)

すなわちshow規則を利用して「rawの中でコードモードに移行する」ことを無理やり実現するわけ4である。このshow規則が有効な状態で​`x`​と書くと、“x”の代わりに「rawでの現在のフォントサイズ」が出力される。

合わせると、以下のようにcurrentを定義5すると#currentでメイン・raw内の「現在のフォントサイズ」が出力されることになる。

#let current = context {
  show raw: it => repr(text.size)
  [#repr(text.size) / `x`]
}

Typstの「フォントサイズの規則」を調べる

動作確認用に以下の文書コードを用意した。

// 現在フォントサイズを表示するやつ
#let current = context {
  show raw: it => repr(text.size)
  [#repr(text.size) / `x`]
}

//// 実験用コード

// raw/text.size = 0.8em
#show raw: set text(size: 8pt+2.5em)
// raw/text.size = 8pt+2em
#show raw: set text(size: 5pt+0.5em)
// raw/text.size = 9pt+1em
#show raw: set text(size: 2em-13pt)
// raw/text.size = 5pt+2em

// text.size = 11pt
#current // "11pt / 27pt"となるはず

#set text(size: 20pt)
// text.size = 20pt
#current // "20pt / 45pt"となるはず

このコードの要点は以下の通りである。

  • rawのtext.sizeへの設定を何度か繰り返すことで、設定の規則が自分の想定通りになっていることを確認する。
  • メインのtext.sizeの2通りの設定の下でメイン・rawの現在フォントサイズを調べることで、rawのtext.sizeが内部的にはem部分を持っている(そこはメインのフォントサイズに換算される)ことを確認する。

実際にTypstでコンパイルすると以下の出力が得られた。

出力結果

このコードの範囲では、全て想定通りに動作していることが判った😊

まとめ

Typstはナニカより簡単😍(簡単😭)


  1. 実際にTypstのコードモードでtext.sizeと書くことでこのパラメタの現在コンテキストでの値を取得することができる。もちろん「現在コンテキスト」が確立している(例えばcontext式の中にいる)必要がある。
  2. TeXCSSにおいては「長さの値」は絶対的な部分しか持たず、“em単位”は常にその場で解釈される。例えばTeXにおいてem値が12ptのときに\dimen0=1emの代入を行ったとすると、\dimen0がもつ値は単に12ptとなる。これに対してTypstでlet len=1emの代入を行ったとするとlenがもつ値は“1em”そのものとなり、これの実際の長さは「現在のコンテキスト」に応じて変化する。
  3. 実際にTypstでコードモードで(8pt+2em)*0.5+5ptという式の値は9pt+1emとなる。
  4. なお、rawに対するshow規則のコードは「raw独自のtext関連パラメタ」が適用された状態で実行される。show規則の仕様で、この状態のコンテキストが暗黙的に確立されている。
  5. このcurrentが関数ではなく「単一のcontent値」であることにも注意してほしい。currentは「コンテキストに依存するcontent値」であるため、書いた場所によって全く異なる出力になることが可能である。

jlreqしてjlreq-deluxeしてpxchfonしてunicodeする方法

アレForumの質問についての話。

その問題

jlreqクラスの文書でjlreq-deluxeパッケージ1を利用して多書体化している状況において、pxchfonパッケージ和文フォントを変更する場合に、unicodeオプションを指定して“Unicode直接”モードに切り替えると、エラーが発生する。

※jlreq-deluxeを使っていない場合は大丈夫である2

% upLaTeX文書, UTf-8 (以下の例も同じ)
\documentclass[uplatex,dvipdfmx,a4paper]{jlreq}
\usepackage{jlreq-deluxe}
% これはエラーになる
\usepackage[yu-win10,unicode,jis2004]{pxchfon}
\begin{document}
巷で噂の葛飾区☃
\end{document}

(エラーメッセージ)

! Font JT2/hmc/m/n/10=zu-jlreq--upnmlminrn-v at 10.0pt not loadable: Metric (TF
M/OFM) file not found.

pxchfonにおいてnoto-otcsourcehan等の “AI0なフォント3 のためのプリセットを指定する場合はunicodeが自動で有効になる4ため、そういうプリセットを指定したときも同じエラーになる。

\documentclass[uplatex,dvipdfmx,a4paper]{jlreq}
\usepackage{jlreq-deluxe}
% 'unicode'が自動で有効になるのでエラーになる
\usepackage[noto-otc,jis2004]{pxchfon}
\begin{document}
巷で噂の葛飾区☃
\end{document}

その対策方針

前提として、pxchfonパッケージで“Unicode直接指定”モード(unicodeオプション)を有効にする際にはpxufontパッケージが読み込まれて和文フォント設定5が変更される。この設定変更のことをここでは仮に Unicode直接化” と呼ぶことにする。

件のエラーの原因は「そもそもjlreq-deluxeが提供する和文フォント設定が“Unicode直接化”をサポートしていない(そのための和文TFM/VFが提供されていない)」からである。だから「必要な和文TFM/VFを自分で作成する」ことをしない限り完全に解決することは不可能である。

完全な解決は不可能なので次善策を考えることにする。そもそも“Unicode直接化”の目的は「“AJ1を利用した機能”をベストエフォートでエミュレートする」ことである。従って​「“AJ1を利用した機能”を諦める」​という前提であれば“Unicode直接化”を行わずに“AI0なフォント”を使うことができる。

実際に、TeX Liveでは“そういう設定”を行うためのdvipdfmxのマップファイル群が用意されている。$TEXMF/fonts/map/dvipdfmx/ptex-fontmapsの下にある“AI0なフォント”用のマップファイルがそれである。

  • noto/*.map: 「Noto Serif/Sans CJK JP」OTF版
  • noto-otc/*.map: 「Noto Serif/Sans CJK JP」OTC
  • sourcehan/*.map: 「源ノ明朝/角ゴシック」OTF版
  • sourcehan-otc/*.map: 「源ノ明朝/角ゴシック」OTC

この次善策を利用する場合に気を付けないといけないのが“AJ1を利用した機能”である。これは以下のものを含むが、特にクオートに注意が必要である。

  • \CID命令
  • 文字出力命令の\aj~の一部6
  • ルビ用仮名・エキスパート仮名字形
  • クオート文字: ‘ ’ “ ”

もし仮にこれらの文字が使われたとすると、それが「想定通りのフォントで出力される」ことはありえないので「何か別のことが起こる」ことになる。特に「他のフォントで出力される」場合は見た目上は正常なので厄介である。先述のTeX Liveのマップファイルを真っ当に(kanji-config-updmapで)設定した場合は非埋込で出力される。「dvipdfmxの-fオプションで指定する」などの用法の場合は、指定の方法により異なり、例えば「既定のフォントマップ設定に従う(別のフォント7になる)」こともありえる。このため、先述の次善策を利用する場合は、出力PDFについて「想定された以外のフォントが使われていないか」を綿密にチェックする必要があるだろう。

その対策方法

ここからは、先述の「次善策」をpxchfonの機能を利用して実行する方法を説明する。

方法1:TeX Liveのマップファイルを読み込む

pxchfonには「既存のマップファイルを読み込んでそれをプリセットの代わりにする」という機能(ファイルプリセット機能)がある。先述のTeX Liveのマップファイルを利用するには、pxchfonの読込時に以下のオプションを指定すればよい。

  • tl:noto: 「Noto Serif/Sans CJK JP」OTF版
  • tl:noto-otc: 「Noto Serif/Sans CJK JP」OTC
  • tl:sourcehan: 「源ノ明朝/角ゴシック」OTF版
  • tl:sourcehan-otc: 「源ノ明朝/角ゴシック」OTC

※この方法を利用した場合、“AJ1を利用した機能”の文字は非埋込になる。

tl:noto-otcを使用した例を示す。これは2004JIS字形で出力される。

\documentclass[uplatex,dvipdfmx,a4paper]{jlreq}
\usepackage{jlreq-deluxe}
\usepackage[tl:noto-otc,jis2004]{pxchfon}
\begin{document}
巷で噂の葛飾区☃\par
\bfseries
巷で噂の葛飾区☃\par
\end{document}

※jlreq-deluxeの既定動作ではjapanese-otfパッケージをjis2004付で読み込む。事態が複雑になるのを避けるため、pxchfonのjis2004の指定は原則としてjapanese-otfに合わせるべきである8

出力結果

もし敢えて90JIS字形(2000JIS字形)にしたい場合は以下のように各パッケージにオプションを指定する。

\documentclass[uplatex,dvipdfmx,a4paper]{jlreq}
% 既定動作の'jis2004'を無効にする
\usepackage[jis2004=false]{jlreq-deluxe}
% 'jis2004'を付けない
\usepackage[tl:noto-otc]{pxchfon}
\begin{document}
巷で噂の葛飾区☃\par
\bfseries
巷で噂の葛飾区☃\par
\end{document}

出力結果

方法2:“Unicode直接化”を抑止する

pxchfonではunicodeオプションを指定して“Unicode直接指定”を有効にした場合は“Unicode直接化”を実施する(pxufontを読み込む)のであるが、この“Unicode直接化”を敢えて無効にする設定も用意されている。上級者向けの設定でありマニュアルにすらちゃんと説明がされていないのであるが、今回の「次善策」の実現に利用できるのでここで紹介しておく。

unicodeオプション(またはそれを自動有効化するプリセットオプション)と一緒に以下のようにオプションを指定する。

  • legacycode-replace=false,legacycode=ignore: “Unicode直接化”を無効にする。“AJ1を利用した機能”については既定のフォントマップ設定(別のフォント)が適用される。

noto-otcプリセットを使用した例を示す。2004JIS字形の場合は以下の通り。

\documentclass[uplatex,dvipdfmx,a4paper]{jlreq}
\usepackage{jlreq-deluxe}
\usepackage[noto-otc,legacycode-replace=false,legacycode=ignore,jis2004]{pxchfon}
\begin{document}
巷で噂の葛飾区☃\par
\bfseries
巷で噂の葛飾区☃\par
\end{document}

出力結果

90JIS字形にしたい場合は以下の通り。

\documentclass[uplatex,dvipdfmx,a4paper]{jlreq}
\usepackage[jis2004=false]{jlreq-deluxe}
\usepackage[noto-otc,legacycode-replace=false,legacycode=ignore]{pxchfon}
\begin{document}
巷で噂の葛飾区☃\par
\bfseries
巷で噂の葛飾区☃\par
\end{document}

出力結果

まとめ

ワタシハ pxchfon チョット デキル🙃


  1. 「jlreqの機能を損なわずにjapanese-otfパッケージの多書体化を適用した」状態を実現するためのパッケージ。
  2. upLaTeXでかつjapanese-otfが不使用の場合はそもそも“Unicode直接化”(後述)が不要であるため、pxufontを読み込んでも設定は変わらない。
  3. “AI0なフォント”というのは詳しくいうと「内部グリフエンコーディングAdobe-Identity-0であるCFFグリフのOpenTypeフォント」のことであるが、ここでは単純に「Noto CJKや源ノは“ちょっと特殊な”フォントである」と考えてほしい。
  4. “AI0なフォント”は「AJ1を前提とする“CMap指定”」を適用できず、従ってそういうフォントを使うためにはunicodeを有効にする必要があるからである。
  5. ここでいう“和文フォント設定”というのは(DVIウェアでなく)LaTeX側の設定の一種で、例えば「“明朝太字”に対してどういう和文TFMを対応させるか」のようなものを指す。
  6. 例えば\ajMaru{1}は最終的には\CID{7555}を実行する。直接“①”を書いた場合はそれは“Unicodeとして”出力される。
  7. もちろん、逆にそういう動作を利用するというテクニックも成立するであろう。
  8. ただしpxchfonのファイルプリセットの仕様に少し問題があってjis2004指定はプリセットには実は効いていない。つまり、jis2004が有効な場合は本来は(u)ptex-noto-otc-04.mapの方を採用するべきであるが、現在の仕様では常に(u)ptex-noto-otc.mapが採用されてしまう。ただしjapanese-otfを普通に用いている場合は実際にはこのマップファイルの違いが問題になることはないので、取りあえずはこのままで大丈夫である。

LaTeXでイロイロな色で塗り潰した黒板太字を書く件

もしかしたら(人類およびエ~アイによる)需要があるかもしれないので、Gistに公開して記事にしておいた。

ソレ(tcarebbパッケージ)

tcarebbパッケージの読込方法はいつも通りでパッケージオプションは存在しない。ただし、ドライバオプションをグローバルオプションに指定する必要がある。

% 例えばdvipdfmxを使う場合
\documentclass[dvipdfmx,uplatex,a4paper]{jsarticle}
\usepackage{tcarebb}

機能一覧。

  • \arebb{«色»}{«文字»}:[通常] 指定の色で塗りつぶされた黒板太字の英大文字(AZのどれか)を出力する。色の指定の方法はTikZと同じ(xcolorパッケージの色式)。

使用例(pdfLaTeX使用)。

%#!pdflatex
\documentclass{article}
\usepackage{amsmath,amsfonts,amssymb}
\usepackage{tcarebb}
\newcommand\TEST[1]{%
  \mathbf{#1}
  \subsetneq \arebb{red}{#1}
  \subsetneq \arebb{blue}{#1}
  \subsetneq \arebb{green!50!black}{#1}
  \subsetneq \mathbb{#1}
}
\begin{document}
\begin{gather}
  \TEST{Z} \\
  \TEST{Q} \\
  \TEST{R}
\end{gather}
\end{document}

出力結果

イカンジ(多分🙃)

まとめ

☃️⛄️「それやってる暇があるならナントカの日のネタをとっとと完成させろ💢💢💢」


補足:ざんねん🙃なコードの話

tcarebbパッケージのコードの大半を占めているのはTikZによるグリフの描画命令である。これはmsbm10というフォント(amsfontsの\mathbbの実体)のグリフパスを基にしている。

[tcarebb.sty:864行目]
\tczab@def@letter{R}{7.22224,6.88889}{%
\fill
(2.67853,3.13828)..controls(2.67853,3.13828)and(3.03833,3.13828)
..(3.03833,3.13828)..controls(3.03833,3.13828)and(4.5375,0.81955)
..(4.5375,0.81955)..controls(4.63745,0.65964)and(4.87732,0.26985)
..(4.98726,0.11993)..controls(5.05722,0)and(5.0872,0)
..(5.31708,0)..controls(5.31708,0)and(6.69632,0)
..(6.69632,0)..controls(6.87622,0)and(7.02614,0)
..(7.02614,0.1799)..controls(7.02614,0.25986)and(6.96617,0.32982)
..(6.87622,0.34981)..controls(6.50642,0.42976)and(6.0167,1.0894)
..(5.77682,1.40923)..controls(5.70686,1.50917)and(5.20714,2.16881)
..(4.5375,3.24821)..controls(5.42702,3.40813)and(6.28654,3.78792)
..(6.28654,4.97726)..controls(6.28654,6.3665)and(4.81735,6.84624)
..(3.638,6.84624)..controls(3.638,6.84624)and(0.49973,6.84624)
..(0.49973,6.84624)..controls(0.31982,6.84624)and(0.15991,6.84624)
..(0.15991,6.66634)..controls(0.15991,6.49643)and(0.34981,6.49643)
..(0.42976,6.49643)..controls(0.99945,6.49643)and(1.04942,6.42647)
..(1.04942,5.92674)..controls(1.04942,5.92674)and(1.04942,0.9195)
..(1.04942,0.9195)..controls(1.04942,0.41977)and(0.99945,0.34981)
..(0.42976,0.34981)..controls(0.34981,0.34981)and(0.15991,0.34981)
..(0.15991,0.1799)..controls(0.15991,0)and(0.31982,0)
..(0.49973,0)..controls(0.49973,0)and(3.23822,0)
..(3.23822,0)..controls(3.41812,0)and(3.56804,0)
..(3.56804,0.1799)..controls(3.56804,0.34981)and(3.39813,0.34981)
..(3.2882,0.34981)..controls(2.7185,0.34981)and(2.67853,0.42976)
..(2.67853,0.9195)..controls(2.67853,0.9195)and(2.67853,3.13828)
..cycle
(2.67853,5.95673)..controls(2.67853,6.16661)and(2.67853,6.49643)
..(3.29819,6.49643)..controls(4.14772,6.49643)and(4.56749,6.14662)
..(4.56749,4.96727)..controls(4.56749,3.67798)and(4.25766,3.48808)
..(2.67853,3.48808)..controls(2.67853,3.48808)and(2.67853,5.95673)
..cycle;
\fill[\tczab@color]
(4.60747,3.628)..controls(4.88731,3.9978)and(4.9173,4.52751)
..(4.9173,4.96727)..controls(4.9173,5.447)and(4.85733,5.97672)
..(4.5375,6.38649)..controls(4.94728,6.29654)and(5.93674,5.98671)
..(5.93674,4.97726)..controls(5.93674,4.32762)and(5.6369,3.8179)
..cycle;
\fill[\tczab@color]
(1.31927,0.34981)..controls(1.39923,0.52971)and(1.39923,0.80956)
..(1.39923,0.8995)..controls(1.39923,0.8995)and(1.39923,5.94673)
..(1.39923,5.94673)..controls(1.39923,6.04668)and(1.39923,6.31653)
..(1.31927,6.49643)..controls(1.31927,6.49643)and(2.45865,6.49643)
..(2.45865,6.49643)..controls(2.32872,6.33652)and(2.32872,6.13663)
..(2.32872,5.98671)..controls(2.32872,5.98671)and(2.32872,0.8995)
..(2.32872,0.8995)..controls(2.32872,0.79956)and(2.32872,0.52971)
..(2.40868,0.34981)..controls(2.40868,0.34981)and(1.31927,0.34981)
..cycle;
\fill[\tczab@color]
(3.4481,3.13828)..controls(3.51807,3.14827)and(3.55804,3.15826)
..(3.638,3.15826)..controls(3.78792,3.15826)and(4.0078,3.17825)
..(4.15771,3.19824)..controls(4.30763,2.95837)and(5.38704,1.17935)
..(6.21658,0.34981)..controls(6.21658,0.34981)and(5.24712,0.34981)
..(5.24712,0.34981)..controls(5.24712,0.34981)and(3.4481,3.13828)
..cycle;
}

このデータを作成するのは大変そうであるが、実はMetaPostを利用すると簡単に作成できる。以下のようなMetaPostのプログラムを用意する。

batchmode;
picture p;
for c=64 upto 90:
  show "\@BEGIN-" & decimal c;
  p := glyph c of "msbm10" scaled 0.01;
  show p;
  show "\@END-" & decimal c;
endfor
end

このプログラムを実行すると、ログファイルに以下のような出力が得られる。

※“R”(文字コード82)の冒頭の部分。

>> "\@BEGIN-82"<msbm10.pfb>
>> Edge structure at line 8:
Filled contour :
(2.67853,3.13828)..controls (2.67853,3.13828) and (3.03833,3.13828)
 ..(3.03833,3.13828)..controls (3.03833,3.13828) and (4.5375,0.81955)
 ..(4.5375,0.81955)..controls (4.63745,0.65964) and (4.87732,0.26985)
 ..(4.98726,0.11993)..controls (5.05722,0) and (5.0872,0)
 ..(5.31708,0)..controls (5.31708,0) and (6.69632,0)
 ..(6.69632,0)..controls (6.87622,0) and (7.02614,0)
 ..(7.02614,0.1799)..controls (7.02614,0.25986) and (6.96617,0.32982)
 ..(6.87622,0.34981)..controls (6.50642,0.42976) and (6.0167,1.0894)
 ..(5.77682,1.40923)..controls (5.70686,1.50917) and (5.20714,2.16881)
 ..(4.5375,3.24821)..controls (5.42702,3.40813) and (6.28654,3.78792)

目的のコードとほぼ同じものが得られていることがわかると思う。

(ナントカの日のネタのタイトルを入力)

(ゆきだるまの前フリを入力する)……ゆきだるま!

というわけで、今年も案の定ゆきだるまの日がやってきました!

(本題の見出しを入力する)

LaTeXの困難な問題を入力する)

(画期的な解決策を入力する)

というわけで、つくりました💪

(本質的⛄な使用例を入力する)

出力結果(素敵😊)

トッテモ本質的で、しかも素敵😊

まとめ

(本質的⛄なまとめ(てない)を入力する)💁

野生の難解TeX言語クイズ(アゲイン)

「野生の難解TeX言語クイズ」の続編です🙂

先日、「TeX言語入門入門」という記事が公開されました。

そこにTeX言語のプログラミングの例としてFizzBuzzが載っています。このコードがかなりアレみ🍣の強いものだったので、クイズを出題しました(えっ😲)

問題の準備

件のFizzBuzzのプログラムはTeXINIモード用のもので、実行すると以下のように動作します。

  • 標準入力から正の整数値を読み込む。これがFizzBuzzの「上限値」となる。
  • 1から「上限値」までのFizzBuzzの結果を標準出力に出力する。

プログラムコードは次の通りです。

\catcode`\{=1%
\catcode`\}=2%
\catcode`\#=6%
\count255 = 255%
\def\newcount#1{\advance\count255by-1\countdef#1=\count255}%
\immediate\write18{read -r line <&0 && echo "$line" > line.tmp && echo}%
\relax%
\newcount\maxnum
\newcount\inter%
\newcount\modval%
\newcount\tempval%
\newcount\remthree%
\newcount\remfive%
\newcount\remfifteen%
\def\temp{}%
\openin0=line.tmp%
\read0 to\temp%
\maxnum=\temp%
\closein0%
\inter=0%
\def\domod#1#2#3{%
    \ifnum#2=0
    \else%
    \ifnum#1=0
    \else%
        \modval=#1%
        #3=#1%
        \divide\modval by#2%
        \multiply\modval by#2%
        \global\advance#3 by-\modval%
    \fi\fi%
}%
\def\fizzbuzz{%
    \advance\inter by1%
    \ifnum\inter=0
    \else%
    \domod\inter3\remthree%
    \domod\inter5\remfive%
    \domod\inter{15}\remfifteen%
    \ifnum\remfifteen=0
        \immediate\write18{echo FIZZ BUZZ}%
    \else%
        \ifnum\remthree=0
            \immediate\write18{echo FIZZ}%
        \else%
            \ifnum\remfive=0
                \immediate\write18{echo BUZZ}%
            \else%
                \immediate\write18{echo \the\inter}%
            \fi%
        \fi%
    \fi%
    \fi%
    \ifnum\inter<\maxnum%
        \fizzbuzz%
    \fi%
}%
\immediate\write18{echo }%
\immediate\write18{echo 1}%
\ifnum\maxnum=1
\else%
\fizzbuzz%
\fi%
\end

標準入出力の部分でわざわざシェル実行(\write18)を利用しているのがフツーではない感じですが、元の記事によると、これは「標準入出力」を厳密に捉えたからで、TeX言語でフツーに「標準入出力」の役割で使われる「オープンされてないストリームに対する\read\write」(つまり\read-1とか\write16とかを指します)は状況によっては標準入出力にならないからのようです。

ここでは出題の都合のため、問題に無関係な部分について以下のように簡略化します。

  • 「標準入出力」についてはフツーの「TeX言語的な標準入出力」(つまり\read-1とか\write16)に置き換える。
  • INIモードではなく、フツーにplainやLaTeXを前提にする。
  • 「行末の%」について、制御綴で終わる行に%を付けるのは明らかに冗長なので除去する。

問題の本題

簡略化した後のプログラムは以下の通りです。

% \newcount はplain/LaTeXのものを利用する
%
\newcount\maxnum
\newcount\inter
\newcount\modval
\newcount\tempval
\newcount\remthree
\newcount\remfive
\newcount\remfifteen
%
% 端末から整数値を入力する
\def\temp{}
\read-1 to\temp
\maxnum=\temp
%
\inter=0%
\def\domod#1#2#3{%
    \ifnum#2=0
    \else
    \ifnum#1=0
    \else
        \modval=#1%
        #3=#1%
        \divide\modval by#2%
        \multiply\modval by#2%
        \global\advance#3 by-\modval
    \fi\fi
}%
\def\fizzbuzz{%
    \advance\inter by1%
    \ifnum\inter=0
    \else
        \domod\inter3\remthree
        \domod\inter5\remfive
        \domod\inter{15}\remfifteen
        \ifnum\remfifteen=0
            \immediate\write16{FIZZ BUZZ}%
        \else
            \ifnum\remthree=0
                \immediate\write16{FIZZ}%
            \else
                \ifnum\remfive=0
                    \immediate\write16{BUZZ}%
                \else
                    \immediate\write16{\the\inter}%
                \fi
            \fi
        \fi
    \fi
    \ifnum\inter<\maxnum
        \fizzbuzz
    \fi
}%
%
\immediate\write16{}%
\immediate\write16{1}% ←(1)
\ifnum\maxnum=1 % ←(2)
\else
\fizzbuzz
\fi
%
% plainとLaTeXの両方で終了する
\csname stop\endcsname \end

一見すると割とフツーのFizzBuzzの実装に見えますが、よく見ると最後の「\FizzBuzz再帰ループに入る」部分のコードがチョット異様です。

  • (1)では「1」の出力だけ別に扱っている。
  • (2)では「上限値」が1であるかを判定している。

\FizzBuzz再帰ループは以下のような構造になっています。

  • \interの初期値は0である。
  • 先頭で\interをインクリメントしている。
  • 末尾で上限検査をしている。

ここから考えるとループの初回は「\interが1のときの処理」なので、(1)や(2)の特別扱いは不要な気がします。

しかし実際にはこの(1)や(2)がないとプログラムの動作は想定仕様を満たさないものになります。まあ実際(1)をわざわざ書いてあるということは「\FizzBuzzを実行しても『1』は(なぜか)出力されない」のでしょう。ここで問題です。

(2)の行を \iffalse に変更した(つまり(2)の判定を無効化した)上で、
「1」を上限値として入力した場合、
このプログラムの出力はどうなるでしょうか。

問題の正解

実際にやってみると、「1」を入力すると、プログラムは​「1」と「2」を出力します。

1
2

問題の解説

事態をヤヤコシイことにしているのは、\FizzBuzzの定義本体の先頭行のコレです。

    \advance\inter by1%

この行の中の1は数値を表しますが、その後に%があるため「終結の空白文字」がありません。以下でこれの影響を調べますが、その前に、TeX​「数値の終結」​の仕様について復習しましょう。

数値の終結の話

\def\nice{8*}

%(A)
\nice\count@=42 \nice

%(B)
\nice\count@=42\nice

(A)の段落では整数を表す数字列42は直後にある空白文字で終結しています。従って、TeXはこの空白文字(トークン)を読んだ時点で整数値を「42」と確定させてcount@への代入を実行します。結果的に、\count@には42が代入され「8*8*」が印字1されます。代入が2つ目の\niceを展開する前に行われることにも注意してください。

対して(B)の段落では42終結させる空白文字がありません。従ってTeX42を読んだ後で数字を探すために後続のトークン列を読むことになります。直後にあるのはマクロ\niceですが、TeX文字トークンを探しているのでこれは展開されて8*となります。8は数字なので数字列の一部として読まれます。次の*は数字でないので、この時点で数字列が終結して\count@への代入が実行されます。結果的に、\count@には428が代入され「8**」が印字されます。今の場合、代入は2つ目の\niceの展開の後に行われました。

数値がなかなか終結しない話

ではFizzBuzzのコードの話に戻ります。後半の部分((2)を\iffalseで置き換えたもの)を再度示します。

\def\fizzbuzz{%
    \advance\inter by1% ←(3)
    \ifnum\inter=0 % ←(4)
    \else % ←(5)
        \domod\inter3\remthree
        \domod\inter5\remfive
        \domod\inter{15}\remfifteen
        \ifnum\remfifteen=0
            \immediate\write16{FIZZ BUZZ}%
        \else
            \ifnum\remthree=0
                \immediate\write16{FIZZ}%
            \else
                \ifnum\remfive=0
                    \immediate\write16{BUZZ}%
                \else
                    \immediate\write16{\the\inter}%
                \fi
            \fi
        \fi
    \fi % ←(6)
    \ifnum\inter<\maxnum % ←(7)
        \fizzbuzz % ←(8)
    \fi
}%
%
\immediate\write16{}%
\immediate\write16{1}% ←(1)
\iffalse % ←(2)を書き換えた
\else
\fizzbuzz % ←(9)
\fi % ←(10)

クイズの出題に従って、端末で「1」を入力すると、\maxnumの値が1、\interの値が0である状態で(9)の\FizzBuzzが実行(展開)されます。

\advance\inter by1\ifnum\inter=0␣\else \domod ……

(3)の行末に空白文字がないため、1から始まる数字列を読む際にTeXは「何か展開不能トークンが現れるまで後ろのトークン列を展開し続ける」ことになります。

(4)の\ifnum\inter=0␣の判定はどうなるでしょうか。今は\interへの代入文(\advanace)の実行の途中ですが、まだ代入は行われていないので、\interの値は0であり、\ifnum真になります。真なのでそのまま続きを読みます。

% (4)の \ifnum が真
\advance\inter by1\else \domod\inter3\remthree ……

展開不能トークンが見つからないまま((4)のifに対応する)(5)の\elseに到達したので、(4)のifに対応する(6)の\fiまで読み飛ばします。

\advance\inter by1\ifnum\inter<\maxnum \fizzbuzz \fi \fi ……

※最後にある\fiは(10)の行によるものです。

またif文が現れました。(7)の\ifnum\inter<\maxnumの判定については、\interは相変わらず0であり\maxnumは1なので真となります

% (7)の \ifnum が真
\advance\inter by1\fizzbuzz \fi \fi ……

\fizzbuzzはマクロであり展開可能なので展開します。

% 1回目の(7)の \ifnum が真
\advance\inter by1\advance\inter by1\ifnum\inter=0␣……

なんと、1回目の\advanceの実行の途中なのに“2回目”の\advanceが出てきてしまいました😲

そして、\advanceは展開不能トークンなので、ここでようやく1から始まる数字列が終結することになり、数値は「1」に決まったので1回目の\advanceが実行されて、\interの値が0から1に変わります。

これで「再帰ループの1回目」の実行が終わったことになります。想定に反して、「1」の出力は行われず、さらにループの2回目に突入してしまいました。

% 1回目の(7)の \ifnum が真
\advance\inter by1\ifnum\inter=0␣\else \domod ……

1から始まる数字列を読もうとしてまた1回目と同様の状況になります。ただし今度は\interの値は1なので今度は(4)の\ifnumは偽となり、(5)の\elseまで読み飛ばされます。

% 1回目の(7)の \ifnum が真
% 2回目の(4)の \ifnum が偽
\advance\inter by1\ifnum3=0␣\else \ifnum\inter=0␣\else \modval=\inter ……

詳細は省略しますが、2つの\ifnumを処理(どちらも偽になる2)した後に展開不能トークンである\modval(countdefトークン)に到達します。ここでようやく数値が「1」と確定して\interの値が1から2に変更されます。

ここから先は「2回目のループがあった場合の3想定された動作」と一致するはずで、つまり「2」が端末に出力されます。(6)の\fiまで到達した後の動作をみましょう。

% 1回目の(7)の \ifnum が真
\ifnum\inter<\maxnum \fizzbuzz \fi \fi \fi ……

\interは2で\maxnumは1なので判定は偽となり1つ目の\fiまで読み飛ばされて2回目の(7)の\ifnumが完了し、次の\fiにより1回目の(7)の\ifnumも完了して、これで(9)の\FizzBuzzの実行が完了したことになります。最後の\fiは(10)に由来するもの((2)の\iffalseに対応する)です。

問題がようやく終結した話

改めて何が出力されたかを振り返ってみると以下のようになります。

  • (1)で「1」が出力された。
  • (9)の\FizzBuzzの実行で「2」が出力された。

まとめ

皆さん、難解TeX言語コードをドンドン書いていきましょう!💁(ええええっ😲)


  1. 数値を空白文字で終結させた場合はその空白文字は吸収されます。つまり空白文字トークンは実行されないので出力に空白は含まれません。
  2. ただしここでも\interは2ではなく1であることに注意が必要です。
  3. \maxnumが1の場合は2回目のループはないはずなのですが🙃

数式でフェニキア文字するパッケージする方法

もしかしたら、フェニキア文字とかを数式で使うLaTeXパッケージをエ~アイが実装できた方がいいかもしれないので、tcmathphnxパッケージつくり方について簡単に解説することにする。

tcmathphnxパッケージはTeX Liveに収録済のphoenicianパッケージのフォント(およびLaTeXフォント定義)を使っている。このように、TeX Liveで(テキストフォントとして)既に “準備ができている” ものを利用する形にすると「パッケージだけをインストールすればよい1」ことになり、利便性の点でも都合がよい。以下に述べる実装手順は「LaTeXで既にテキストフォントとして\usefont命令を用いて利用できる」ようなフォント2であれば通用するはずである。

前提知識

本記事では以下の知識を仮定する。

  • フツーTeX言語の知識。
  • LaTeXの低層のフォントの取扱(NFSS)の知識。TeXレベルのフォント(TFM)の知識は不要である。

手順①:NFSSでのシェープ指定を調べる

まずは、対象のフォント(つまり「phoenicianパッケージで実際に使われているフォント」)のシェープ指定をどうにかして把握する。シェープ指定というのは\usefontで使われるOT1/cmr/m/nのような値のことである。これにはとにかくパッケージの実装コードを精読することが大事である。

まずはphoenicianパッケージの仕様に沿った動作を追うことにする。パッケージの説明書によると、phoenicianを利用してアーレプ(𐤀)・ベート(𐤁)・ギーメル(𐤂)を(左横書き字形3で)出力する場合には次のように書くことになっている。

{\phncfamily abg}

見た感じ、フォントファミリを指定しているのが\phncfamilyっぽいので、これの定義を調べればファミリの定義が判りそうである。phonetician.styの中を見て\phncfamilyの定義を探してみる。

[phoenician.sty:28行目]
\newcommand{\phncfamily}{\usefont{OT1}{phnc}{m}{n}}

おっと、命令名は“~family”であるが実体は(ファミリでなく)シェープを\usefontで完全指定するものであった🙃 とにかくこれでシェープ指定がOT1/phnc/m/nであることが判明した。

手順②:文字コードを調べる

「使いたいフォント」の正体が判ったので、今度は「使いたい文字」の文字コード(符号位置)を調べることにする。TeXのレガシーフォントが前提であるため、文字コードは0~255の範囲の整数である。

話を簡単にするため、以降では「使いたい文字」を​「アーレプ・ベート・ギーメルの右横書き字形」​に限定することにする。

※歴史的にフェニキア文字は左横書き(left-to-right)と右横書き(right-to-left)で異なる字形を用いていて、phoenicianパッケージではその両方をサポートしている。最終的には右横書きが定着したため、一般的にはフェニキア文字は右横書きと認識されていて4右横書き字形の方がよく知られている。このため、数式は左横書きであるが5敢えて右横書き用の字形を採用することにする。

phoenicianパッケージでは、右横書き字形を使いたい場合は\AR~という名前の命令を使うことになっている。

% 右横書き字形で左横書きしている
{\phncfamily \ARaleph\ARbeth\ARgimel}

phoenician.styの中でこの\AR~の定義を探すと、次のようになっている。

[phoenician.sty:33~36行目より抜粋]
\chardef\ARaleph=`A
\chardef\ARbeth=`B
\chardef\ARgimel=`G

アーレプ・ベート・ギーメルはABG文字コード(数値6でいうと65・66・71)に割り当てられている。

手順③:パッケージの仕様を決める

以上で数式パッケージ実装に必要な情報が揃ったので、次に自分がつくるパッケージの仕様と実装方針7を決めることにする。

  • パッケージ名: tcmathphnx
  • 名前空間接頭辞8tczpx

既存仕様においてラテン文字以外の文字(例えばギリシャ文字ヘブライ文字)がどう扱われているかを考えてみると、例えば\pi\alephのように「文字名に基づく、その文字を出力するための命令」が用意されている。基本的にこれを踏襲することにするが、ただしヘブライ文字と区別するため文字名の前にphnxを付けた9ものを命令名とする。つまり次の3つの命令を提供することになる。

  • \phnxaleph: アーレプ(𐤀)を出力する。
  • \phnxbeth: ベート(𐤁)を出力する。
  • \phnxgimel: ギーメル(𐤂)を出力する。

もちろんこれらの命令は、\pi\alephと同様に「数式中でそれを書いたら自動的に適切なフォントが選ばれる」必要がある。

% 明示的なフォント指定無しで使える
$\phnxaleph + \phnxbeth + \phnxgimel = 0$

そして実装方針についても「\alpha\alephと同様の方式」にする。すなわち「数式記号命令10」として実装する。

手順④:数式記号フォントを定義する

いよいよtcmathphnxパッケージの実装に取り掛かる。まずはいつものパッケージ宣言を書く。

% パッケージ宣言
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{tcmathphnx}[2025/07/13 v0.1]

新たなフォントに紐づくような数式記号命令を定義するには、まずそのフォントを「数式記号フォント」として定義する必要があり、これには\DeclareSymbolFontという命令を用いる。

\DeclareSymbolFont{«数式記号フォント名»}{«エンコーディング»}{«ファミリ»}{«シリーズ»}{«シェープ»}

最初の引数の«数式記号フォント名»は自分が使う名前であるので(既存のものと被らなければ)何でもよい。ここでは名前空間接頭辞と同じ文字列のtczpxとする。後ろの4つの引数は紐づけたいフォントのシェープ定義であり、手順②においてそれがOT1/phnc/m/nであることを把握している。従って、次の文を実行すればよいことになる。

% 数式記号フォント'tczpx'を定義
\DeclareSymbolFont{tczpx}{OT1}{phnc}{m}{n}

手順⑤:数式記号命令を定義する

次に、\phnxaleph等を数式記号命令として定義したい。数式記号命令を定義するには\DeclareMathSymbolという専用の命令を用いる。

\DeclareMathSymbol{\命令}{«種別»}{«数式記号フォント名»}{«文字コード»}

これで\命令が「«数式記号フォント名»のフォントの«文字コード»の位置の文字を出力する」という命令として定義される。«種別»は「当該の記号の数式中での振る舞い」を規定するもので、次の8種類の値の何れかを指定する。

  • \mathalpha: 数式英字11
  • \mathord: 数式英字以外の通常の記号・文字
  • \mathop: 大型演算子
  • \mathbin二項演算子
  • \mathrel: 関係演算子
  • \mathopen: 開き括弧類
  • \mathclose: 閉じ括弧類
  • \mathpunct: 句読点類

今回定義したいのは“文字”であるため«種別»\mathordにする。\phnxalephを「数式記号フォントtczpxAの位置の文字を出力」する命令として定義するには以下の文を実行すればよい。

% 数式記号命令の定義
\DeclareMathSymbol{\phnxaleph}{\mathord}{tczpx}{`A}
% 文字コードを数値で指定したい場合
%\DeclareMathSymbol{\phnxaleph}{\mathord}{tczpx}{65}

\phnxbeth\phnxgimelの定義も同様に行える。

\DeclareMathSymbol{\phnxbeth}{\mathord}{tczpx}{`B}
\DeclareMathSymbol{\phnxgimel}{\mathord}{tczpx}{`G}

以上でtcmathphnxパッケージの実装は完了である🙂

動作確認してみる

改めてtcmathphnxパッケージの実装コードを全てまとめて示す。

% パッケージ宣言
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{tcmathphnx}[2025/07/13 v0.1]
% 数式記号フォント'tczpx'を定義
\DeclareSymbolFont{tczpx}{OT1}{phnc}{m}{n}
% 数式記号命令の定義
\DeclareMathSymbol{\phnxaleph}{\mathord}{tczpx}{`A}
\DeclareMathSymbol{\phnxbeth}{\mathord}{tczpx}{`B}
\DeclareMathSymbol{\phnxgimel}{\mathord}{tczpx}{`G}

ひとまず要求仕様(3文字だけ)を満たすパッケージが完成したので動作確認をしてみる。以下のような文書ソースをpdflatexでタイプセットしてみよう。

%#!pdflatex
\documentclass[a5paper]{article}
\usepackage{tcmathphnx}
\begin{document}
\[
  \frac{\phnxaleph}{\phnxbeth + \phnxgimel} +
  \frac{\phnxbeth}{\phnxaleph + \phnxgimel} +
  \frac{\phnxgimel}{\phnxaleph + \phnxbeth} = 4
\]
\end{document}

出力結果

カンペキ😊

実際のtcmathphnxパッケージ

本記事に掲載したコードでは3文字しか使えないが、全てのフェニキア文字を使えるようにこれを拡張したのが、Gistに実際に公開されているtcmathphnxパッケージである。

※記号の定義のコード(\tczpx@do)がチョットアレ🤯な感じになっているが、この部分は以下のようにフツーに大量の\DeclareMathSymbolを書いたのと全く同等である。

\DeclareMathSymbol{\phnxaleph}{\mathord}{tczpx}{`A}
\DeclareMathSymbol{\phnxbeth}{\mathord}{tczpx}{`B}
\DeclareMathSymbol{\phnxgimel}{\mathord}{tczpx}{`G}
\DeclareMathSymbol{\phnxdalet}{\mathord}{tczpx}{`d}
\DeclareMathSymbol{\phnxhe}{\mathord}{tczpx}{`e}
\DeclareMathSymbol{\phnxvaf}{\mathord}{tczpx}{`f}
……(以下同様)……

まとめ

エ~アイがイロイロなLaTeXパッケージをつくれるようになるといいですね😊


  1. TeXシステムにおいてフォントとその周辺のファイルをインストールする手順は、多くの場合極めてヤヤコシイものになる。
  2. ただし「Unicode LaTeXUnicodeのフォントを利用する」パターンについてはスコープ外とする。
  3. フェニキア文字の左横書き字形と右横書き字形の区別については後ほど説明する。なお、この直前の文章の「アーレプ(𐤀)」の部分にあるUnicodeテキストのフェニキア文字は右横書き字形で表示されているはずである。
  4. Unicode標準でもフェニキア文字は右横書きの用字系と規定されていて、このためフェニキア文字Unicodeフォントに収録されている字形は通常は右横書きのものである。ちなみに、エトルリア文字(等の古代イタリア地方の用字系を統合した“Old Italic”)についてはUnicodeは左横書きと規定していて、コード表の字形も左横書き用になっている。
  5. \aleph\beth等の「数式用のヘブライ文字」も本来は右横書き用の字形を左横書きで使っているわけである。
  6. TeX言語の整数表記規則において`Aという表記は「A文字コード」である65を意味する。
  7. TeX言語でのプログラミングは制約が多いので、仕様を考える際に同時に「この仕様はどうすれば実装できるのか」について思案しておくのが望ましい。
  8. 本記事で紹介するコードの範囲では名前空間接頭辞は使っていない(リリース版のコードでは使っている)が、数式記号フォント名としてこの名前を利用している。
  9. フェニキア文字ISO 15924用字系コードphnxである。
  10. 「数式記号命令とは何か」の説明は割愛する。この後の文で「数式記号命令は\DeclareMathSymbolで定義する」と説明したが、逆に考えて「\DeclareMathSymbolで定義されるのが数式記号命令」と把握するという手もあるだろう。
  11. 「数式英字」とは「\mathbf等の数式フォント命令の適用対象となる文字」のことである。今回定義する命令はフォントが固定でないと意味がないため数式英字ではない。

イマドキのエ~アイのTeX知識を添削してみた(4)

前回までは実用的なTeX言語プログラミングを題材にしてきたわけですが、TeX言語においては芸人的なプログラミングもそれと同じくらい重要な能力です(多分 🙃)

というわけで、本記事ではTeX芸的な課題を扱います。

イマドキのAIのFizzBuzzを添削する

まずは、例のラング・ラグーの記事にも出てきたFizzBuzzです。例の記事と同じく「FizzBuzzとは何か」の説明はしませんが、組版的な要求やインタフェースに関する仕様は明示します。

※この問題ではGemini 2.5 Proを使用しました。

入力したやつ

以下の内容をMarkdown形式で表して入力しました。

以下の条件に従って、TeX言語でFizzBuzzをするマクロを実装してください。

  • \FizzBuzz{<整数n>}で1からnまでのFizzBuzzの結果を版面に出力する。
  • 各単語の間は空白で区切り、最初と最後に改段落を入れる。
  • LaTeX上で動くマクロを作る。パッケージを使ってはいけない。

出力されたやつ

最終的なプログラムコードだけを載せます。
\makeatletter
% --- 内部で使用するカウンタと条件フラグを定義 ---
\newcount\@fizzbuzz@i      % 1からnまでループするためのカウンタ
\newcount\@fizzbuzz@temp   % 割り算の中間計算用カウンタ
\newif\if@fizzbuzz@printed % "Fizz"か"Buzz"が出力されたかを判定するフラグ

% --- \FizzBuzz{n} マクロの定義 ---
% 1からnまでのFizzBuzz結果を出力します
\def\FizzBuzz#1{%
  \par % 1. 出力の前に改段落
  \@fizzbuzz@i=1 % カウンタを1で初期化
  \loop % 2. ループを開始
    % --- 各数値に対するFizzBuzz処理 ---
    \@fizzbuzz@printedfalse % フラグをリセット
    
    % 3の倍数か判定
    \@fizzbuzz@temp=\@fizzbuzz@i
    \divide\@fizzbuzz@temp by 3
    \multiply\@fizzbuzz@temp by 3
    \ifnum\@fizzbuzz@temp=\@fizzbuzz@i
      Fizz%
      \@fizzbuzz@printedtrue
    \fi
    
    % 5の倍数か判定
    \@fizzbuzz@temp=\@fizzbuzz@i
    \divide\@fizzbuzz@temp by 5
    \multiply\@fizzbuzz@temp by 5
    \ifnum\@fizzbuzz@temp=\@fizzbuzz@i
      Buzz%
      \@fizzbuzz@printedtrue
    \fi
    
    % "Fizz"でも"Buzz"でもなければ、数値そのものを出力
    \if@fizzbuzz@printed\else
      \the\@fizzbuzz@i
    \fi
    % --- 処理ここまで ---
    
  \ifnum\@fizzbuzz@i < #1 % 3. カウンタがn未満ならループを継続
    \space % 次の単語との間に空白を挿入
    \advance\@fizzbuzz@i by 1 % カウンタを1増やす
  \repeat % ループの先頭に戻る
  \par % 4. 全ての出力の後に改段落
}
\makeatother

添削してみるやつ

ロジックは完璧です😃

  • マクロ定義中に空の行があるが、これだと余計な改段落が起こるので削除した1
  • 40行目で引数の数値#1を空白文字で終結2させているが、#1が内部値で与えられる可能性を考慮して\relaxに置き換えた。
  • 例によって\makeatletterイラネ😐
添削結果

修正後のコード:

実行例
\documentclass{article}
\usepackage{ai-coding-3}
\begin{document}
\FizzBuzz{1}
\FizzBuzz{8}
\FizzBuzz{18}
\FizzBuzz{100}
\end{document}

出力結果
カンペキ😍

イマドキのAIのNabeAzzを添削する

例の記事ではFizzBuzzに続いてNazeAzzを出題していますが、結果的に「NabeAzzを知っているか」という知識問題という形で終わっています。ここでは知識ではなくプログラミングを調べたいので、元々の問題文(要件が書いてある)の文章を与えることにします。

※ただしLaTeXフォーマットを前提にした上で若干条件を整理しました。

※この問題ではGemini 2.5 Proを使用しました。

入力したやつ

以下の内容をMarkdown形式で表して入力しました。

以下の動作を行うTeX言語のマクロ\NabeAzzを作れ。

\NabeAzz{<整数n>}は1からnまでの整数の十進表記を順に空白区切りで出力する。ただし、整数が「3の倍数である」または「(その十進表記に)数字3を含む」場合は、フォントを「Computer Modern Funny Italic」でサイズを\largeに変えて出力する。その他の条件は以下の通り。

  • LaTeXフォーマットを前提とする。パッケージを使用してもかまわない。
  • 引数の<整数n>は正であり、かつ数字列で与えられると仮定してよい。想定外の引数を与えた場合の動作は何も規定しない。
  • 「Computer Modern Funny Italic」のTeXでのTFM名)はcmfi10である。LaTeX では「エンコーディングOT1、ファミリcmfr、シリーズm、シェープit」に割り当てられている。

出力されたやつ

最終的なプログラムコードだけを載せます。
%=============== フォント定義 ===============
% Computer Modern Funny Italic (cmfi10) を OT1/cmfr/m/it に割り当て
\DeclareFontFamily{OT1}{cmfr}{}
\DeclareFontShape{OT1}{cmfr}{m}{it}{<-> cmfi10}{}

%=============== マクロ定義 ===============
\ExplSyntaxOn

\NewDocumentCommand{\NabeAzz}{ m }
 {
  \int_step_inline:nn { #1 }
   {
    \int_compare:nF { ##1 == 1 } { ~ }
    \bool_if:nTF
     {
      ( \int_mod:nn { ##1 } { 3 } == 0 )
      ||
      ( \str_if_contains_p:nn { \int_to_arabic:n {##1} } { 3 } )
     }
     {
      { \large \fontfamily{cmfr}\fontshape{it}\selectfont \int_to_arabic:n {##1} }
     }
     {
      \int_to_arabic:n {##1}
     }
   }
 }

\ExplSyntaxOff

添削してみるやつ

Geminiの回答には次のような文章が含まれていました。

実装には expl3 プログラミング層(xparseパッケージを通じて読み込まれます)を利用します。これにより、数値計算や文字列の判定が容易になります。

この問題では今までと違って「パッケージを使用してもかまわない」としたわけですが、するとAIはexpl3を使ってきたようです😲

なんかスゴそうなので、早速実行してみると……

! Missing number, treated as zero.
<to be read again>
                   \__bool_=_1:
l.4 \NabeAzz{40}

?

……ざんねん🙃

アホになる条件

現状のコードでは“アホになる”(CM Funny Italicで出力される)整数の条件が次のようになっています。

    \bool_if:nTF
     {
      ( \int_mod:nn { ##1 } { 3 } == 0 )
      ||
      ( \str_if_contains_p:nn { \int_to_arabic:n {##1} } { 3 } )
     }

一見合ってそうですが、よく見ると前の条件(3の倍数)で条件関数が抜けています。ここは\int_compare:~の述語形式(_p)を補う必要があります。

      ( \int_compare_p:n { \int_mod:nn { ##1 } { 3 } == 0 } )

後の条件では\str_if_contains:~の述語形式が書いてあって正しそうですが、実はexpl3にこの名前の関数はありません😲 部分文字列判定の関数の名前は正しくは\str_if_in:~です。では\str_if_in_p:nnと書けばよいのかというと、それも不正解です。expl3の規則として、述語形式が使える条件関数は完全展開可能なものに限られるからです。(\str_if_in:nnは完全展開可能ではありません。)簡潔に書くのは難しそうなので、条件文形式を用いてbool変数\l_tmpa_boolに判定結果を一旦代入することにします。(もっといい方法がある?🤔)

    % "3を含むか"の判定結果を \l_tmpa_bool に代入
    \str_if_in:nnTF { \int_to_arabic:n {##1} } { 3 }
     { \bool_set_true:N \l_tmpa_bool }
     { \bool_set_false:N \l_tmpa_bool }
    % 整数 ##1 のアホ性の判定
    \bool_if:nTF
     {
      ( \int_compare_p:n { \int_mod:nn { ##1 } { 3 } == 0 } )
      ||
      \l_tmpa_bool
     }

とりあえずこれだけ修正すれば、動作としては正常になります。

展開制御な話

ところが、先ほどのコードの「3を含むか」の部分にはまだ問題があります。

    \str_if_in:nnTF { \int_to_arabic:n {##1} } { 3 }

ここで引数##1には何らかの「整数を表すトークン列」(例えば6とか\c_one_intとか)が与えられるはずですが、ここでは仮に123としましょう。

    \str_if_in:nnTF { \int_to_arabic:n {123} } { 3 }

ここで判定したいのは「\int_to_arabic:n {123}の値である1233を含むか」であるはずです。しかし\str_if_in:nn~(およびexpl3の標準関数の慣習)では「str型の引数がトークン列(n引数)で与えられる場合はそのトークン列を展開せずにトークン化した結果が使われる」ことになっています。つまりこの場合は「\int_to_arabic:n {123}という文字列」でこれは意図に反します。意図通りにするには\str_if_in:nn~の第1引数を完全展開する必要があります。\str_if_in:en~は用意されてないので\exp_args:Neを使いましょう。

    \exp_args:Ne \str_if_in:nnTF { \int_to_arabic:n {##1} } { 3 }

なお、引数は何らかの「整数を表すトークン列」と先ほど言いましたが、少なくとも実際の動作としては\int_step_~系の関数において実引数として渡される整数は「典型的な十進表記」の形をしています3。この前提の下では「\int_to_arabic:n {<十進表記>}3を含むか」は「<十進表記>3を含むか」と同値になるので、結局元のままでも動作としては意図通りになるようです。しかし、もし仮にこの性質を仕様として仮定するのであれば\int_to_arabic:n自体が不要であり、コード中の\int_to_arabic:n {##1}は全て##1に置き換えるべきでしょう。

アホなフォントへの切替

「CM Funny Italic」への切替のコードは現状では以下のようになっています。

\fontfamily{cmfr}\fontshape{it}\selectfont

しかし「CM Funny Italic」のシェープは飽くまで“OT1/cmfe/m/it”なので、このコードでは現在状態が「エンコーディングがOT1、シリーズがm4」でないと「CM Funny Italic」が選ばれません5。4属性を全て固定させたいので、代わりに\usefontを用いることにします。

\usefont{OT1}{cmfr}{m}{it}
例によって(ry

これまでの問題と同様に、実装したコードはLaTeXのパッケージとして使えるようにしたいところです。LaTeXのパッケージをexpl3で実装する場合はファイルの先頭に(\ProvidesPacakgeの代わりに)\ProvidesExplPackageを書くというお作法になっています。

\ProvidesExplPackage {ai-coding-4} {2025-07-07} {0.1}
  {NabeAzz by AI coding}

この宣言を書いておくと“expl3用のカテゴリコード”が自動でイイカンジに取り扱われます6。パッケージの中の\ExplSyntaxOnは避けたいので\ProvidesExplPackage宣言を利用しました。

添削結果

修正後のコード:

実行例
\documentclass{article}
\usepackage{ai-coding-4}
\begin{document}
\NabeAzz{40}
\end{document}

出力結果
一件落着😊

まとめ

というわけで、エ~アイに「TeXマクロを書いて」とお願いするとエ~アイがexpl3のコードを書いてくる可能性があるので、皆さん、expl3も学習しましょう!💁(マジで🙂)


  1. ちなみに、(La)TeXのマクロのコード中で「見やすさのために行を空けたい」という場合は「%だけ書いた行」を入れます。%だけの行は完全に無視され動作に影響を与えません。
  2. ちなみに、もしここで誤って#1%のように空白を削ってしまうと、TeXは数字列を終結させるために次の行の\spaceを食ってしまって、結果的に空白が出力されなくなります。
  3. interface3においてこの性質は明記されていません。ただし、\int_step_function:~\int_step_tokens:~の箇所にある例(整数を印字した結果が“1”や“2”になる)は当該の性質を示唆しています。
  4. 「CM Funny Italicという書体」という意味で捉えるならシリーズを固定する必要はないかもしれませんが、何れにしても「CM Funny Italic」にはシリーズmしかありません。
  5. 例えばLuaLaTeXではエンコーディングの既定値がTUであり、この状態では「CM Funny Italic」への切替は失敗します。
  6. 当該のパッケージファイルを読み込んでいるときにだけ\ExplSyntaxOnの状態になり、そこから別のファイルを読み込む場合には自動的に\ExplSyntaxOffに戻ります。