前回の記事 では、ガントチャートアプリの工程表を作りました。今回は、休日アプリのレコードから、その年の年間カレンダーを PDF にする話です。
できあがったのは、下の帳票です。A4 横 1 枚に 12 か月を 4 列×3 段で並べ、日曜(赤字)・土曜(青字)と、休日アプリに登録された日を区分ごとに色分けしています(祝日は赤、休日はオレンジ、その他は緑の背景)。同じ設定のまま、レコードの年を変えれば、その年のカレンダーになります。
上が 2026 年、下が 2027 年のレコードから作った PDF です。2027 年のカレンダーの元にしたレコードは、次のとおりです。区分(祝日・休日・その他)ごとに色が付きます。
今回作ったのは、日本語の年間カレンダーと、休日の区分の色分けまでです。最初に依頼した凡例・休日の名前の一覧・稼働日数・言語の切り替えは入れていません。タイトルも「カレンダー名+年」ではなく「年+年間カレンダー」で、cal_name は使っていません。
また、最初の依頼どおりには進みませんでした。言語の切り替え・凡例・祝日の一覧・月ごとの稼働日まで一度に作らせたところ、計算式が通らず、1 月だけ・日本語だけの月カレンダーから作り直しました。この記事は、その分かれ道と、うまくいかなかった点のまとめです。
| # | やったこと | 結果 |
|---|---|---|
| 1 | 休日アプリのレコードから直接作るか、出力用のアプリを作るかを決めた | レコードから直接(A)を選んだ |
| 2 | 要件を全部入れた年間カレンダーを作らせた | 計算式の検証や実機でエラー。原因を切り分けきれず、方針を変えた |
| 3 | 1 月だけ → 12 か月 → A4 横 4 列×3 段 と積み上げた | 静的な HTML で形を決めてから、計算式で HTML を組み立てる形にした |
| 4 | 休日の色分けを DIC で引くようにした | 各マスで配列を探す処理が無くなった |
| 5 | 実機の PDF を見て直した | 月の範囲外の日付(0 以下、月末より後)が出ていたのを直した |
| 6 | LOG で処理時間を測った | 約 6 秒。年に数回しか作らないので、このままとした |
使ったのは、印刷屋プラグイン Ver.8、Claude Desktop の拡張機能(印刷屋 MCP)0.2.0、tools 2.1.0 です。
1. 依頼と最初の分かれ道
使ったのは rex0220 休日プラグインの休日アプリ(アプリ 845)です。1 レコードが 1 年分で、項目は次のとおりです。
| 項目 | 内容 |
|---|---|
cal_name |
カレンダー名 |
year |
年(数値) |
Table |
休日の一覧(date 日付、name 名前、type 区分 = 祝日 / 休日 / その他 など) |
最初の依頼は、次のとおりでした。
- A4 横 1 枚(A3 も選べる)に 12 か月
- 日曜・土曜・祝日・休日・その他を色分けし、凡例を付ける
- 休日の名前の一覧、月ごとの稼働日数、年間の休日数
- 日本語・英語・中国語の切り替え
- タイトルは「カレンダー名+年」
AI には、作る前に方針を 2 つ出させました。
| 方針 | 内容 |
|---|---|
| A | 休日アプリのレコードから、印刷屋の計算式で直接作る |
| B | 出力用のアプリを別に作り、kintone の計算式で日付を用意してから印刷屋で並べる |
アプリを増やしたくないので A を選び、稼働日は「土日・祝日・休日を除いた日(その他は稼働日)」としました。
2. 全部入れたら、通らなかった
要件を全部入れた設定を作らせたところ、次のようなエラーが続きました。
| 症状 | 分かったこと |
|---|---|
検証で traverse tree operator $NN
|
IF を 5 段入れ子にし、その中で ARRAY_SOME を 3 つ使っていた。MAX で順位を付ける形に変えると通った |
| 実機で帳票が作られない | ループの中で、ARRAY_SOME を 3 つ含む MAX を使っていた。原因は切り分けきれなかった |
e.trim is not a function |
REPLACE に数値を渡していた。"" & work で文字列にすると直った |
| ページの体裁が崩れた | 試しに付けた目印の文字 x が class 名 rex0220-pcraft-page に混ざっていた |
一つ直すと次が出る状態で、どこが原因かを AI と話しても絞り切れませんでした。そこで、要件を減らして 1 か月から作り直すことにしました。
方針の変更: 1 月の月カレンダーだけ、日本語だけ。祝日の表記なし。
3. 1 月から積み上げる
3.1 空白の配列と日付の配列
月カレンダーの組み方は、以前の記事 印刷屋プラグイン CSV_TVAL でカレンダー を参考にさせました。
- 1 日の曜日の数だけ空白を並べた配列と、1〜月末の配列をつなぐ
- 7 個ごとに
</tr><tr>で折り返す
最初は「アプリを見ずに、2026 年 1 月」を静的に作らせ、形が決まってから 12 か月に広げました。
3.2 A4 横 4 列×3 段
12 か月を並べたら、A4 横 1 枚に 4 列×3 段で収めます。デザインは Claude のデザイン(Design)で案を作り、それを印刷屋の CSS に移しました。
Claude のデザインで作った案です。この形を、印刷屋の CSS に移しました。
.cal .grid { display: grid; grid-template-columns: repeat(4, 1fr); column-gap: 28px; row-gap: 14px; }
.cal .g td { font-size: 14px; height: 22px; text-align: center; }
.cal .sun { color: #c0392b; }
.cal .sat { color: #1f5fa8; }
各月を 6 週×7 日 = 42 マスに固定したので、月によって段の高さが変わりません。
3.3 年をレコードから取り、HTML を計算式で組み立てる
ここまでは 2026 年の HTML を静的に持っていました。次に、レコードの year からその年のカレンダーを計算式で組み立てる形にしました。
この段階では、曜日の色だけで、休日の色分けはまだ入れていません。
ループは 12 か月×42 マス = 504 回の 1 回だけにし、k(0〜503)から月とマスを出します。
i = MOD(k, 42) マス(0〜41)
mi = (k - i) / 42 月(0〜11)
w = MOD(i, 7) 曜日(0 = 日)
dd = i - 1 日の曜日 + 1 日付(1 未満と月末より後は空白)
月ごとの「1 日の曜日」「日数」「年初からの日数」は、ループの前に 12 個の配列にしておきます。
offs, ARRAY_FOR(12, mi, MOD(DATE_DIFF("1970-01-04", DATE_ADD(base, mi, "month"), "days"), 7)),
lens, ARRAY_FOR(12, mi, DATE_DIFF(DATE_ADD(base, mi, "month"), DATE_ADD(base, mi + 1, "month"), "days")),
msts, ARRAY_FOR(12, mi, DATE_DIFF(base, DATE_ADD(base, mi, "month"), "days")),
1970-01-04 は日曜日なので、そこからの日数を 7 で割った余りが曜日になります。
4. 休日の色分けは DIC で引く
祝日・休日・その他の色分けは、最初はマスごとに ARRAY_SOME で休日の日付の配列を探していました。2 章のエラーもこの形で出ています。
ここで、必要なデータをキーで DIC に入れておき、各マスでは 1 回引くだけにするよう指示しました。キーは年初からの日数、値は class 名です。
base, year & "-01-01",
kd, DIC_ASSIGN(
ARRAY_DIC(TARRAYIF(type = "その他", date), vv, DIC("" & DATE_DIFF(base, vv, "days"), "oth")),
ARRAY_DIC(TARRAYIF(type = "休日", date), vv, DIC("" & DATE_DIFF(base, vv, "days"), "kyu")),
ARRAY_DIC(TARRAYIF(type = "祝日", date), vv, DIC("" & DATE_DIFF(base, vv, "days"), "hol"))),
DIC_ASSIGN は後のものが上書きするので、その他 → 休日 → 祝日 の順に重ねると、同じ日なら 祝日 > 休日 > その他 になります。また、休日アプリに区分が登録されている日は、曜日(日曜の赤・土曜の青)よりも区分の色を優先しています。
なお、ARRAY_SOME で探していた版はエラーで完成していないので、2 つの方式の処理時間は比べていません。
504 マスのループは次のとおりです。
months, JOIN(ARRAY_FOR(504, k, LET(
i, MOD(k, 42),
mi, (k - i) / 42,
w, MOD(i, 7),
dd, i - ARRAY_GET(offs, mi) + 1,
inm, IF(dd < 1, 0, IF(dd > ARRAY_GET(lens, mi), 0, 1)),
kc, IF(inm = 1, DIC_ITEM(kd, "" & (ARRAY_GET(msts, mi) + dd - 1)), ""),
IF(i = 0, "<div class='m'><div class='mh'><span class='mn'>" & (mi + 1) & "</span><span class='mu'>月</span></div><table class='g'>" & head & "<tbody><tr>", "") &
IF(AND(w = 0, i > 0), "</tr><tr>", "") &
"<td class='" & IF(kc != "", kc, IF(w = 0, "sun", IF(w = 6, "sat", "d"))) & "'>" &
IF(inm = 1, "" & dd, "") & "</td>" &
IF(i = 41, "</tr></tbody></table></div>", ""))), ""),
ループの中の LET は、マスごとの値(月・曜日・日付・区分)を 1 回だけ計算するためです。
5. 実機の PDF で見つかった誤り
tools の検証は「エラー 0」でしたが、実機の PDF では月の前後に -3〜0 や 32〜42 が表示されていました。
範囲外のマスでも休日の DIC を引いていたため、4 月の「33〜36」のように、翌月の祝日(5 月 3〜6 日)の色まで付いていました。
その月の日かどうかを AND(…) で求めて LET の変数に入れ、IF(変数, …) と使っていたのが原因です。あとで小さな式で確かめると、LET の変数に入れた偽や 0 は、IF の条件にそのまま使うと真として扱われました(IF の条件に直接書けば正しく動きます)。
| 式 | 結果 | 期待 |
|---|---|---|
LET(c, 1 = 0, IF(c, "T", "F")) |
T | F |
LET(c, 0, IF(c, "T", "F")) |
T | F |
LET(c, "", IF(c, "T", "F")) |
F | F |
IF(AND(a >= 1, a <= 31), "T", "F")(a = -3) |
F | F |
そこで、1 / 0 で持ち、inm = 1 と比べて書くようにしました。
inm, IF(dd < 1, 0, IF(dd > ARRAY_GET(lens, mi), 0, 1)),
kc, IF(inm = 1, DIC_ITEM(kd, "" & (ARRAY_GET(msts, mi) + dd - 1)), ""),
直したあと、2023〜2027 年のレコードで PDF を作って確かめました。下は 2023 年と、2024 年(うるう年で、2 月 29 日まで出ている)です。どちらも、祝日・休日・その他がそれぞれの色で出ています。
6. 処理時間を測る
ボタンを押してから PDF ができるまでが長いので、LOG で処理時間をコンソールに出しました。時刻は NOW() で取り、前の段階の値に依存させて、順序を保つことをねらいました。ただし、依存させることで NOW() が実際にその時点で評価されることまでは確かめていません。以下の値は、今回の環境での参考値です。
t0, NOW(),
lg0, LOG("年間カレンダー 開始", DATE_FORMAT(t0, "YYYY-MM-DD HH:mm:ss")),
…
t_mon, IF(months = "", NOW(), NOW()),
…
t1, IF(out = "", NOW(), NOW()),
lg1, LOG("年間カレンダー 終了", DATE_FORMAT(t1, "YYYY-MM-DD HH:mm:ss"),
"504マス", ROUND(DATE_DIFF(t_arr, t_mon, "milliseconds") / 1000, 3) & " 秒",
"合計", ROUND(DATE_DIFF(t0, t1, "milliseconds") / 1000, 3) & " 秒"),
DATE_FORMAT は、タイムゾーンを指定しなくても日本時間で出ました。結果は次のとおりです。
| 段階 | 秒 |
|---|---|
| DIC | 0.013 |
| 月の配列 | 0.013 |
| 504 マス | 5.967 |
| 仕上げ | 0.002 |
| 合計 | 5.995 |
ほぼ全部が 504 マスのループです。ループの中身を変えて、それぞれ 504 回を測りました。計測用のボタンを別に作り、A〜G を 1 つの計算式の中で順に計算して、1 回だけ実行した値です。
| # | ループの中身 | 秒 |
|---|---|---|
| A | k |
0.006 |
| B | MOD(k, 42) + 1 |
0.049 |
| C |
LET で変数 1 つ |
0.189 |
| D |
LET で変数 6 つ(月・曜日・日付・範囲・区分の計算) |
2.348 |
| E |
DIC_ITEM 1 回 |
0.264 |
| F | 文字列をつなぐだけ | 0.007 |
| G | 本番と同じ中身 | 15.47 |
中身が増えるほど時間が増えることは分かりますが、この結果から、関数 1 回あたりの時間や、本番の 1 マスあたりの計算の数までは決められません。
また、本番と同じ中身の G は 15.47 秒で、本番の式で測った 5.967 秒の約 2.6 倍になりました。計測用の式では A〜F の変数の計算がどこで行われているかを確かめておらず、評価の順序や計測の条件の影響を切り分けられていません。この差の原因は分かっていません。
今回の計測結果では、504 マスを組み立てる処理に当たる区間が、測った時間のほとんどを占めていました。
カレンダーの PDF を作るのは年に数回なので、約 6 秒はこのままとしました。
7. つまずき
- 一度に全部入れた: 言語切り替え・凡例・一覧・稼働日を一度に入れると、エラーの原因を切り分けられませんでした。1 か月・日本語だけから積み上げるほうが早く進みました
- 検証が通っても値は確かめられない: tools の検証とローカルのプレビューは「エラー 0」でも、範囲外の日付は実機の PDF を見るまで気づきませんでした
-
LET の変数に入れた真偽値: 偽や 0 でも、IF の条件にそのまま使うと真になりました(5 章)。条件は IF に直接書くか、
変数 = 1と比べて書きます - LOG の値: 5 章の確認の途中で、LOG に出た値が式の結果と食い違うことがありました(結果では false の値が LOG では true、IF の結果が LOG では空)。6 章の処理時間も LOG の値なので、参考値として扱います
- AI の説明の誤り: AI は途中で「ループの中の LET の変数は、内側のループから見えない」と説明しましたが、誤りでした。ループの中の LET の変数は内側のループから見え、LET の外側からは見えない仕様です
- 会話の中のプレビュー: リモートのセッションでは印刷屋 MCP のプレビューが表示されず、同じ HTML を手元で再現して確認しました
8. 設定ファイル
今回の設定(年間カレンダーのボタン 1 つ)です。6 章の処理時間を測る LOG と、計測用のボタン(A〜G)は除いています。
使うアプリには、次のフィールドコードが要ります。違う場合は計算式の中を書き換えてください。
| フィールドコード | 種類 | 内容 |
|---|---|---|
year |
数値 | 年 |
date |
日付(テーブル内) | 休日の日付 |
type |
ドロップダウンなど(テーブル内) | 祝日 / 休日 / その他 |
appId・appName は今回のアプリ(845「休日」)の値です。
年間カレンダー_DIC.json(クリックで開く)
{
"date": "2026-10-13 00:00:00",
"pluginName": "印刷屋プラグイン",
"pluginID": "rex0220 Print craft plugin",
"PluginVersion": "8",
"appId": 845,
"appName": "休日",
"pluginEnable": true,
"commonCssEnable": true,
"externalRefs": "block",
"pluginComment": "レコードの年(year)の年間カレンダー(A4 横・4列×3段)。HTML は計算式で組み立て、区分は DIC で引く",
"pluginDescription": "休日アプリの詳細画面で、レコードの年の年間カレンダーの PDF を作ります。",
"pluginInfos": [
{
"state": true,
"menu": "年間カレンダー(DIC)",
"menu_en": "Annual calendar (DIC)",
"mcheck": true,
"list": false,
"authority": false,
"desc": "年間カレンダーを作ります",
"desc_en": "Make the annual calendar",
"viewsCsv": "-",
"tagsInfo": {
"fieldsInfo": [
{
"state": false,
"desc": "ボタン表示条件",
"fieldcode": "$out",
"formulaSet": ""
},
{
"state": true,
"desc": "ファイル名",
"fieldcode": "$fname",
"formulaSet": "year & \"年_年間カレンダー.pdf\""
},
{
"state": true,
"desc": "年間カレンダー",
"css": ".cal { box-sizing: border-box; padding: 40px 48px 36px; color: #1f1f1f; display: flex; flex-direction: column; gap: 18px; }\n.cal .hd { display: flex; align-items: baseline; justify-content: space-between; border-bottom: 2px solid #1f1f1f; padding-bottom: 8px; }\n.cal .yr { font-size: 44px; font-weight: bold; line-height: 1; margin-right: 16px; }\n.cal .tt { font-size: 18px; letter-spacing: 0.3em; }\n.cal .era { font-size: 12px; color: #555; }\n.cal .grid { display: grid; grid-template-columns: repeat(4, 1fr); column-gap: 28px; row-gap: 14px; }\n.cal .mh { border-bottom: 1px solid #9a9a9a; padding-bottom: 2px; margin-bottom: 4px; }\n.cal .mn { font-size: 22px; font-weight: bold; margin-right: 6px; }\n.cal .mu { font-size: 12px; color: #555; }\n.cal .g { width: 100%; border-collapse: collapse; table-layout: fixed; }\n.cal .g th { font-size: 12px; font-weight: bold; padding: 1px 0; text-align: center; border: none; background: none; }\n.cal .g td { font-size: 14px; height: 22px; padding: 0; text-align: center; border: none; }\n.cal .sun { color: #c0392b; }\n.cal .sat { color: #1f5fa8; }\n.cal .hol { color: #ffffff; background: #d9534f; font-weight: bold; }\n.cal .kyu { color: #ffffff; background: #e67e22; font-weight: bold; }\n.cal .oth { color: #1e7b4a; background: #dff3e6; }",
"html": "##page##",
"formulaSet": "LET(\n // 区分は DIC(キー = 年初からの日数、値 = class)に入れて、各マスでは DIC_ITEM で 1 回引く(ARRAY_SOME を使わない)\n // DIC_ASSIGN は後のものが上書きするので、その他 → 休日 → 祝日 の順に重ねると 祝日 > 休日 > その他 になる\n base, year & \"-01-01\",\n kd, DIC_ASSIGN(\n ARRAY_DIC(TARRAYIF(type = \"その他\", date), vv, DIC(\"\" & DATE_DIFF(base, vv, \"days\"), \"oth\")),\n ARRAY_DIC(TARRAYIF(type = \"休日\", date), vv, DIC(\"\" & DATE_DIFF(base, vv, \"days\"), \"kyu\")),\n ARRAY_DIC(TARRAYIF(type = \"祝日\", date), vv, DIC(\"\" & DATE_DIFF(base, vv, \"days\"), \"hol\"))),\n // 月ごとの 年初からの日数・1 日の曜日(0=日)・日数\n msts, ARRAY_FOR(12, mi, DATE_DIFF(base, DATE_ADD(base, mi, \"month\"), \"days\")),\n offs, ARRAY_FOR(12, mi, MOD(DATE_DIFF(\"1970-01-04\", DATE_ADD(base, mi, \"month\"), \"days\"), 7)),\n lens, ARRAY_FOR(12, mi, DATE_DIFF(DATE_ADD(base, mi, \"month\"), DATE_ADD(base, mi + 1, \"month\"), \"days\")),\n head, \"<thead><tr><th class='sun'>日</th><th>月</th><th>火</th><th>水</th><th>木</th><th>金</th><th class='sat'>土</th></tr></thead>\",\n era, IF(year >= 2019, \"令和\" & IF(year = 2019, \"元\", \"\" & (year - 2018)) & \"年\", \"\"),\n // 12 か月 × 42 マス。マスごとの値は LET で 1 回だけ計算する\n months, JOIN(ARRAY_FOR(504, k, LET(\n i, MOD(k, 42),\n mi, (k - i) / 42,\n w, MOD(i, 7),\n dd, i - ARRAY_GET(offs, mi) + 1,\n // その月の日なら 1(LET 変数に入れた偽や 0 は IF の条件で真になるため、1 / 0 で持ち = 1 と比べる)\n inm, IF(dd < 1, 0, IF(dd > ARRAY_GET(lens, mi), 0, 1)),\n kc, IF(inm = 1, DIC_ITEM(kd, \"\" & (ARRAY_GET(msts, mi) + dd - 1)), \"\"),\n IF(i = 0, \"<div class='m'><div class='mh'><span class='mn'>\" & (mi + 1) & \"</span><span class='mu'>月</span></div><table class='g'>\" & head & \"<tbody><tr>\", \"\") &\n IF(AND(w = 0, i > 0), \"</tr><tr>\", \"\") &\n \"<td class='\" & IF(kc != \"\", kc, IF(w = 0, \"sun\", IF(w = 6, \"sat\", \"d\"))) & \"'>\" &\n IF(inm = 1, \"\" & dd, \"\") & \"</td>\" &\n IF(i = 41, \"</tr></tbody></table></div>\", \"\"))), \"\"),\n REPLACE($html, \"##page##\",\n \"<div class='rex0220-pcraft-page cal'>\" &\n \"<div class='hd'><div><span class='yr'>\" & year & \"</span><span class='tt'>年間カレンダー</span></div><span class='era'>\" & era & \"</span></div>\" &\n \"<div class='grid'>\" & months & \"</div></div>\")\n)"
}
],
"filecode": "",
"pageSize": "A4",
"orientation": "l",
"dpi": "96",
"printMode": "preview"
},
"calcInfo": {
"fieldsInfo": []
}
}
]
}
気を付ける点
- 印刷屋の計算式で HTML を組み立てるときは、HTML の属性を
'で囲み、${式}を属性の中に書かない - REPLACE などに数値を渡すときは
"" & 値で文字列にする - LET の変数に入れた真偽値や 0 を、IF の条件にそのまま使わない。
変数 = 1のように比べて書く - ループの中で配列を探す(ARRAY_SOME など)代わりに、ループの前に DIC を作って DIC_ITEM で引く
- 今回の処理時間は、ほとんどが 504 マスのループだった。LOG と NOW() で段階ごとに測ると、どこに時間がかかっているかの目安になる(値は参考値)
まとめ
- 休日アプリのレコードから、印刷屋の計算式だけで年間カレンダー(A4 横 4 列×3 段)を作った。出力用のアプリは作っていない
- 要件を一度に全部入れると通らず、1 月・日本語だけの月カレンダーから積み上げ直した
- 12 か月×42 マス = 504 回のループ 1 回で HTML を組み立て、休日の色分けは DIC で 1 回引くだけにした
- tools の検証が通っても、範囲外の日付は実機の PDF で見つかった。値の確認は実機で行う
- 処理時間は約 6 秒(参考値)で、ほぼ全部が 504 マスのループ。年に数回の帳票なのでこのままとした。凡例・祝日の一覧・稼働日・言語切り替えは今回は入れていない
シリーズ「rex0220 印刷屋プラグイン - AI(Claude Desktop)に〜」:
- チャットで帳票を作らせる
- 帳票のデザインを作り込ませる
- 見積書から注文書・請求書を作らせる
- 組織図を設計させてから、アプリを作らせる
- QRコード付きの資産ラベルを作らせる
- 写真付きの点検報告書を作らせる
- ガントチャートアプリの工程表を作らせる(関連レコード一覧に対応した Ver.8)
- 休日アプリから年間カレンダーを作らせる(この記事)
関連リンク:







