この投稿は TeX & LaTeX Advent Calendar の 21 日目の記事です.
- 20 日目は ZR さんの記事 Hello World あたたたた 20 日目 expl3 編,
- 22 日目は hidaruma さんの記事 Tagged PDF(タグ付 PDF)入門以前 です.
はじめに ・ 記事執筆の経緯 (自分語り)
僕は TeX / LaTeX のことが好きな高校生です(決して詳しい訳ではありません).
中 2 ぐらいの頃に LaTeX を始めて高 1 辺りの時からちゃんとハマり,それからは「黄色い本」や「緑の本」をはじめとした TeX / LaTeX にまつわる本を買い漁って暇なときに勉強したりしていました.
また僕の通っている学校では 3 ヶ月ほど前に運動会があり,僕は運動会の実行委員会でルールブックやプロジェクトノート(当日の審判業務に関わる人たちへ渡す,各競技における審判ごとのシフトや役割をまとめた冊子のことです)を LaTeX で組版したり,クラス同士の対戦組み合わせ表を自動生成するツールを LaTeX(expl3 メイン)で作ったりしていました.
そんなこんなで自分なりに TeX / LaTeX を活用してきた僕ですが,先日ついに「まともに使えるパッケージを自分で作ってみる」ということにも手を出してみました.
という訳でこの記事では,LaTeX 初心者である僕がどのようにして TeX / LaTeX と向き合い,これらを活用してきたのかというテーマについて書かせていただこうかと思っております.
記事の構成上その大半が自分語りになってしまうかと思われますが,どうか温かい目で見守ってくれますと幸いです.
また僕はこの Advent Calendar に寄稿なさっている他の方々と比較して TeX・LaTeX 周りの知識が信じられないほど浅く,また Qiita を使うのも初めてであるため大変お見苦しい記事となってしまうかもしれませんが,大目に見ていただけますと嬉しいです.また僕が間違った事を書いておりましたら遠慮なくご指摘ください.
その 1 : 対戦組み合わせ表の自動組版
1.1 : 背景事情
対戦組み合わせを公平に決めるにはどうすれば良いでしょうか.
もちろん,どの組がどの組と戦うかという事はクラスの勝敗に大きく関わるため公平かつ慎重に決める必要があります.僕が独断で「この組み合わせにしたから皆これに従って!」と言っても,恐らく納得する人間はほとんどいないでしょう.
このやり方ではすべてが僕の裁量に委ねられているため,やろうと思えば僕の所属しているクラスが有利になるような対戦組み合わせをいくらでも作れてしまいます.お世辞にも公平な決め方とは言えなさそうですよね.
そこで僕は「対戦組み合わせの生成を機械(LaTeX)に任せ,各組の応援団長の前でそれを 1 回実行してしまえば,誰からも文句が出ない公平な決め方になるのではないか?」と思い,実装に至りました.
僕の学校の運動会競技は
- 1 クラス vs 1 クラス の試合を計 4 回(僕の学校は計 7 クラスあるので $\lceil 7/2 \rceil = 4$ で 4 回です.そのため 2 回試合を行うクラスがただ 1 つ存在し,それ以外の 6 クラスは 1 回だけ試合をします)行い,その勝敗のみで点数を決定する競技
- 勝ち残り式のトーナメントを行い,その順位に応じて点数を決定する競技
- タイムレース形式で 1 位から 7 位まで順位を決め,その順位に応じて点数を決定する競技
の大方 3 つに分かれています.今回はこのうちの「1 クラス vs 1 クラス の試合を計 4 回行い,その勝敗のみで点数を決定する競技」の対戦組み合わせ生成の実装のみについて考えたいと思います(トーナメント制の場合は 2 回戦うクラスについての考慮が不要な分 1 vs 1 を 4 回行う場合よりも簡単ですし,タイムレース形式の場合はそもそも対戦組み合わせを考える必要そのものがあまりないからです).
1.2 : 実 装
こんな感じでやりました(なお以下のコードでは \ExplSyntaxOn 状況であることを仮定しています):
\seq_new:N \l__match_class_seq
\seq_new:N \l__match_at_first_seq
\seq_set_from_clist:Nn \l__match_class_seq {
1組, 2組, 3組, 4組, 5組, 6組, 7組
}
\tl_new:N \l__match_item_tl
\int_new:N \l__match_count_int
\int_set:Nn \l__match_count_int { 0 }
\newcommand{\match}{
\int_gincr:N \l__match_count_int
\tl_set:Ne \l__match_item_tl {
\seq_rand_item:N \l__match_class_seq
}
\int_compare:nT { \l__match_count_int <= 2 } {
\seq_gput_right:NV \l__match_at_first_seq { \l__match_item_tl }
}
\seq_if_empty:NTF \l__match_class_seq {
\seq_rand_item:N \l__match_at_first_seq
} {
\tl_use:N \l__match_item_tl
}
\seq_gremove_all:NV \l__match_class_seq { \l__match_item_tl }
}
1.3 : コードの簡単な説明
上から順番に説明していきます.
- 最初の上 2 行は使用する変数(シークエンス型)の宣言です.とくに説明はいらないかと思います.
- 3--5 行目では各クラスの名前をシークエンス型の変数
\l__match_class_seqに保存しています.僕の学校は各学年で 1 組から 7 組までの計 7 クラスが存在するためこのようにしました. - そこから下にある 3 行も最初の上 2 行と同様に使用する変数名を宣言しているだけなので説明は割愛します.
- 次から(
\matchの定義)が本題です.
\matchは大まかに言うと,実行するたびに 1 組から 7 組までのいずれかを重複を許さずランダムに 1 つ出力するコマンドです.ただし\macthを計 8 回以上実行している場合には例外処理を行います(この処理については後述します).- カウンタ
\l__math_count_intをインクリメントします(このカウンタの存在理由については後述します) - 関数
\seq_rand_item:Nでシークエンス\l__match_class_seqの要素をランダムで 1 つ取得し,それを\tl_set:Neでトークンリスト\l__macth_item_tlに保存しています(ここで関数\tl_set:Neの引数の中身にある\seq_rand_item:N \l__match_class_seqが展開されていなければならないため引数指定子をNeにしています).
要するにこれは「 { 1 組, 2 組, ..., 7 組 } のうちからいずれか 1 つをランダムに取得し,それを\l__match_item_tlに保存している」ということです. - 次に
\int_compare:nT { \l__match_count_int <= 2 } { ... }で,カウンタ\l__match_count_intの値が 2 以下であるか否かを調べ,それが真であれば\seq_gput_right:NV \l__match_at_first_seq { \l__match_item_tl }によりシークエンス\l__match_at_first_seqの要素に先ほどセットした\l__match_item_tlの中身の値を追加します.ここで\seq_gput_rightではトークンリスト変数\l__match_item_tlの "中身の値" を参照しなければならないため引数指定子をNVにしてあります.
なぜこのような事をする必要があるのかと言いますと,前述した通り僕の学校の運動会では 1 vs 1 を 4 回行う際に 2 試合戦わなければならないクラスが必ず 1 つ発生します.また 2 試合戦うクラスは生徒の準備や体力面の事情から 1 試合目と 4 試合目に競技を行うことが一般的です.そのため「最初に戦う 2 クラス(\l__match_count_intの値が 2 以下であるとき)の組名を\l__match_at_first_seqに保存し,\matchを 8 回以上使用して\l__match_class_seqの中身が空になった場合はこの\l__match_at_first_seq(1 試合目に戦ったクラス)からランダムに 1 クラスを取り出して出力する」という処理を加える必要があります. -
\seq_if_empty:NTF \l__match_class_seq { ... } { ... }で,\l__match_class_seqの中身が- 空であった場合(すなわち
\matchが 8 回以上実行されている場合)は\seq_rand_item:N \l__match_at_first_seqにより\l__match_class_seq
(1 試合目に戦った 2 クラスの組名を保存しているシークエンス)からランダムで 1 クラスを出力(つまりこのクラスが 2 試合戦うクラスになります)し, - 空でなかった場合(すなわち
\matchの実行回数が 7 回以下の場合)は\l__match_item_tl(クラス名のリスト\l__match_class_seqから要素をランダムに 1 つ取得し保存したもの)の中身そのものを出力します.
- 空であった場合(すなわち
-
\seq_gremove_all:NV \l__match_class_seq { \l__match_item_tl }で,シークエンス\l__match_class_seqから\l__match_item_tlの中身に相当するものを削除します.
- カウンタ
以上により,所望の操作を行うコマンド \match が適切に定義できました.
1.4 : 使 用 例
前述のコードをプリアンブルに貼り付け,以下のコードを実行すると出力例のようになるかと思われます.
\begin{document}
\begin{center}
\noindent
{%
\Huge\bfseries\gtfamily
{\normalfont 第1試合:}\match vs \match\\[\baselineskip]
{\normalfont 第2試合:}\match vs \match\\[\baselineskip]
{\normalfont 第3試合:}\match vs \match\\[\baselineskip]
{\normalfont 第4試合:}\match vs \match\\[\baselineskip]
}%
\end{center}
\end{document}
出力例:
また,このソースコードをタイプセットする度に出力結果(n 組 vs m 組の部分)が変わることを見て取れるでしょう.
1 試合目で戦ったクラスのこともきちんと第 4 試合に反映されています.
ちなみにですが,この \match コマンドは 9 回以上実行されることを想定しておりません(恐らく 9 回目以降は 1 試合目で戦ったクラス(\l__match_at_first_seq の要素)のうちいずれか 1 つが延々とランダム出力され続けるのではないかと思います).
まあ実際に使う分にはこの程度でも不都合はないので….
1.5 : 後 日 談
ちなみに,このシステムを構築?した後各クラスの応援団長の前でこのコードを実行して対戦組み合わせを決定したのですが,このシステムのおかげか対戦組み合わせの不公平等で揉めるようなことは一切ありませんでした.要するに全員この方針とこのシステムで納得してくれたという訳です.やったね! 多分こんな事しなくても良かった
その 2 :自作 ? パッケージの公開 (晒し)
これから紹介するコードは非常に乱雑で不完全です.一応最低限の用途では使えるように何とか調整は施したつもりですが,とても実用に値するものではありません.あくまで「初心者の学習記録」としてご覧ください.
2.1 : 背景事情
ある日ふと『 LaTeX2ε マクロ&クラス・プログラミング実践解説(通称 "緑の本")』を読んでいると,「不特定多数のボックスの使用」というお題で「項目の幅から 1 行に配置する項目の個数を自動で算出し,それらの項目を各行へ等間隔で配置する」という autoalign 環境なるものが紹介されておりました.分かりやすく言うと項目同士を横に並べる enumerate 環境のようなもので,もっと言えば tasks 環境に近い感じです.
そこで僕はこの autoalign 環境に目をつけ,この環境を自分が使いやすいように改造してみようと思い立ちました.
2.2 : 実 装
ここに貼るには流石にソースコードが長すぎるので,自戒の念も込めて GitHub に上げ(晒し)ました.
https://github.com/Ueyama-Eiji/autoenumitem/blob/main/autoenumitem.sty
(ガチのマジでひどいコードなので閲覧注意です)
「緑の本」で解説されている autoalign 環境からの変更点は以下の通りです:
- 実装を expl3 メインにしてみました.
- 寸法や整数の計算は TeX 言語と比べて遥かに楽になってて感動しました.
- その一方でボックスの書き方はあまり楽になっていない(むしろ旧来の TeX な書き方のほうが書きやすいかも?) 気がします…
- 緑の本で触れられていたものには enumerate 環境のように各項目に番号が振られておりましたが,折角なので番号を振らない横並びの itemize 環境的なものも作りました.
- label の形式や enumerate / itemize 環境における labelsep, itemsep, leftmargin 等にあたるパラメーター,また上下のアキなどを keyval 形式でユーザーが自由に指定できるようにしました(オプション引数です).またカラム数も指定できますし,何なら autoenumi 環境ではラベルの開始番号も指定できます.
-
\Aitemや\Aenumのオプション引数にnoautoを指定することで,その項目だけ独立して 1 行に(いわゆる "ぶち抜き" で)配置できるようにしました(しかし色々と強引かつ例外的な処理を行っているためあまり使用は推奨できません.どうしても環境内で長い項目を配置したい時にご使用ください).
実装の説明に関しては著作権等の都合もあり,僕がどこまで話していいのか分からないためここでは行わないことにしました(ソースコード本体の方に申し訳程度の説明を書いてあるのでそちらを参照してください).
2025-12-29 追記:これらの autoenumi / autoitemi 環境を少々改善した autolist パッケージを GitHub に公開しました(これでも実用からは程遠いですが…).
主な変更点は次のとおりです:
- ぶち抜きの際に,従来は
\Aitem/\Aenumのオプション引数にnoautoを指定する仕様であったところを,\Aitem*,\Aenum*のように各制御綴にスター*を後置する仕様に変更しました. - ぶち抜き配置の際の挙動を改善しました(具体的には,ぶち抜きの際に項目を list 環境に入れることで数式等がずれて配置されなくなるようにしました).
-
\Aitem,\Aenumのオプション引数に文字列を入れることで,ラベルを一時的にその文字列へ変更できるようにしました.
(ちなみに余談なのですが,これを実装する際に人生で初めて\afterassignmentを使いました.\afterassignmentなんて存在を知った当初は「こんなん何に使うんだ?」と思っていたのですが,やはりプリミティヴとして存在している以上必要な場面はちゃんとあるんですね…) - autoenumi 環境の列数および各項目を入れるボックス幅の計算方法を改善しました.
2.3 : 使 用 例
細かいオプションの説明等は面倒くさいので省略します.
\documentclass{jlreq}
\usepackage{autoenumitem}
\usepackage{luatexja-otf}
\begin{document}
\noindent ああああああ
\begin{autoitemi}
\Aitem ああああああ \Aitem いいいいいい \Aitem うううううう
\Aitem[noauto] えええええええ
\Aitem[noauto] おおおおおおおおおおおおおおおおおおおおおおお
おおおおおおおおおおおおおおおおおおおおおおおおお
\Aitem かかかかかかか \Aitem ええええええええええええ
\end{autoitemi}
うううううう
\begin{autoitemi*}
\Aitem ああああああ \Aitem いいいいいい \Aitem うううううう
\Aitem[noauto] えええええええ
\Aitem[noauto] おおおおおおおおおおおおおおおおおおおおおおお
おおおおおおおおおおおおおおおおおおおおおおおおお
\Aitem かかかかかかか \Aitem ええええええええええええ
\end{autoitemi*}
いいいいいいいいいいいいい
\begin{autoenumi}
\Aenum あああああああああ \Aenum いいいいいいいいい \Aenum うううううううううう
\Aenum おおおお \Aenum かかかっかかっかっか
\Aenum[noauto] おおおおおおおおおおおおおおおおお
おおおおおおおおおおおおおおおおおおおおおおおおおおおお
\end{autoenumi}
あああああああああ
\begin{autoenumi}[label=\ajKakko{\arabic{Aenumi}}, leftmargin=3em]
\Aenum あああああああああ \Aenum いいいいいいい \Aenum ううううううう
\Aenum[noauto] おおおおおおおおおおおおおおおおおおおおおおおおおおおおおお
おおおおおおおおおおおおおおおおおおおおおお
\end{autoenumi}
おおおおおおおおおおおおおおおおお
\begin{autoenumi}[label=(\roman{Aenumi}), skip=.5\baselineskip]
\Aenum[noauto] あああああああああああああああああああああああああああああ
あああああああああああああああああああああああああああ
\Aenum いいいいいいいいい \Aenum ううううううううう \Aenum ええええええええええ
\end{autoenumi}
ほげほげほげほげ
\begin{autoenumi}[columns=2, start=4]
\Aenum あああああああ \Aenum いいいいいいいいい
\Aenum うううううううう \Aenum ええええええええええええ
\Aenum おおおおおおおおお \Aenum かかかかかかかかかかか
\Aenum きききき
\end{autoenumi}
おおおおおおおおおおおおおおおおお
\begin{autoitemi}[label=♡, labelsep=1em, itemindent=1em]
\Aitem ああああああああ \Aitem いいいいいいいいいいい
\Aitem[noauto] えええええええええええええええええええええええ
ええええええええええええええええええええええええ
\Aitem かかかかかかっかか \Aitem きききききききき
\end{autoitemi}
\end{document}
このソースコードをタイプセットすると次のような出力結果が得られるかと思います:
ここで環境名にスターがつく場合とつかない場合の挙動の変化について軽く触れておきます.
デフォルトの autoitemi / autoenumi 環境では列数の微調整機能(\@autoitemi@columnadjust, \@autoenumi@columnadjust)がそれぞれ有効になっていますが,スターがつく場合はこれらが無効になります.
そのため,スター付きにすると場合によってはスターなしの時よりもより項目同士が左に寄った出力が得られるのではないでしょうか.
さいごに
この件を通して思ったことはとにかく「まともに使えるパッケージ作ってる人ってほんとにスゲー」ということです.
今回自分は初めて「他人に使ってもらう」ことを(一応)想定したパッケージ(笑)作りをしてみたのですが,いくら調べてみてもバグや想定外の挙動が無くなりませんでした.というか今でも list 環境で \item の直後に autoenumi 環境を使った場合の挙動は怪しいですし,多分 noauto 状態で quote 環境を使ったりするとインデントがおかしくなると思います(2025-12-29 追記:この問題は(多分ですが)解消しました).
こういった不具合をほぼ完璧に無くして多くのユーザーに使ってもらえるような便利なパッケージを作ることは絶対に並大抵の知識じゃ出来ないことだと思いました.
いつもは存在が確約されていて普段何気なく使っているパッケージやクラスファイル等も,その中身を覗いてみると存外に摩訶不思議な大魔境だったりします.こうして僕らが日頃当たり前のように便利な TeX / LaTeX を使えるというのは決して当たり前の事ではなく,開発者さん達の知識や努力の上に積み上げられたかけがえのない財産なのだという事を痛感いたしました.
僕もいつかは知識と実力を身につけて TeX や LaTeX を "支える" 側についてみたいと思うばかりです.
最後に,記事の投稿が期日から約 5 日も遅れてしまったことを心よりお詫び申し上げます.本当に申し訳ございませんでした.

