19 · JXLS から xl3 へ — JavaScript の選択肢
シナリオ
チームが JVM 上で JXLS を使って Excel
レポートを生成していて、同じことを Node.js やブラウザでも必要になった —
あるいは「JXLS for JavaScript」で検索したら node-java を包んだ 8 年前の
ラッパーしか見つからなかった、という状況です。xl3 はメンテナンスされて
いる答えです。スプレッドシート自身がテンプレートになる Excel-to-Excel
テンプレートエンジンです。
機能が偶然重なったわけではありません。xl3 の仕様は JXLS が約 10 年かけて 踏んだエッジケースを項目単位で取り込んでいます — 結合されたデータ行の セル、名前付き範囲、印刷範囲、アウトラインレベル、複数行テキストには それぞれ専用の ADR と適合性フィクスチャがあります。運用原則は (ADR-0034) JXLS の経験は借りる、構文は借りない です。
モデルの違いを表 1 つで
| JXLS | xl3 | |
|---|---|---|
| ディレクティブの置き場所 | セルのコメント (jx:each(items="rows" lastCell="D4")) — グリッド上では見えない | セルの値 ({{ @filter [Status] = "Open" }}) — 見える、レビューできる、diff が取れる |
| 式言語 | JEXL (${employee.payment * 1.1}) — 新たに覚える 2 つ目の言語 | Excel の構文 ({{ [Payment] * 1.1 }}, IF, XLOOKUP, SUM) — テンプレート作成者がすでに知っているもの |
| データの供給元 | コードでバインドした Java オブジェクト (context.putVar("employees", list)) | 2 つ目の .xlsx、または stdin から渡す JSON — render(template, data) は純粋関数。同じ入力なら同じバイト列 |
| ブロックの境界 | 明示的な lastCell="D4" 座標 | {{ ... }} マーカーから推論(必要なら明示的に {{ @block A:D }}) |
| 抜け道 | カスタム Java コマンド — チューリング完全で移植不可 | 設計上なし — テンプレートはどの実装でもレンダリングできる引き継ぎ成果物のまま (ADR-0048) |
結果として、JXLS のテンプレートはセルのコメントと Java のバインディングを 編集できる人、つまり開発者が所有します。xl3 のテンプレートは スプレッドシートを編集できる人が所有します。
ディレクティブ対応表
| JXLS | xl3 での書き 方 | 備考 |
|---|---|---|
jx:each(items="rows" var="r" lastCell=…) | データブロック — {{ [Column] }} マーカーを含むテンプレート行 | ループ宣言そのものがありません。ブロックがソース行 1 行あたり出力 1 行に展開されます。はじめに 参照 |
${r.name} | {{ [Name] }} | ソース行の列参照 |
${r.amount * 1.1} | {{ [Amount] * 1.1 }} | JEXL ではなく Excel の演算子 |
セルに付けた jx:if(condition=…) | {{ IF([Renewal] > 10000, "Priority", "Standard") }} | 条件付きセル |
行を落とすために使った jx:if | {{ @filter [Status] = "Open" }} | 複数の @filter は AND で結合されます |
orderBy 付きの jx:each | {{ @sort [Total] desc }} | |
groupBy 付きの jx:each | {{ @group [Region] }} + {{ @subtotal SUM([Renewal]) }} | 小計行を間に挟み、N 段のネストにも対応 — グループと小計 |
jx:each(direction="RIGHT") | {{ @repeat right 3 }} | |
| 複数コレクション | ブロックごとに {{ @source Renewals }}、そして {{ @join Customers on Customers[Account] = Renewals[Account] }} | マルチソース + @join |
jx:multisheet | パターンを |