結論
2026年9月22日(火)は祝日です。しかし自作の祝日テーブルから抜けていました。
その結果、営業日を遡って算出する日付が最大4日ズレていました。単体テストは全件通っていました。
営業日計算を実装している方は、自分の祝日データに 2026-09-22 が入っているか確認してみてください。
なぜ2026年9月22日が祝日なのか
祝日法(国民の祝日に関する法律)第3条第3項にこう定められています。
その前日及び翌日が「国民の祝日」である日(「国民の祝日」でない日に限る。)は、休日とする。
前後を祝日に挟まれた平日は、それ自体が休日になります。通称「国民の休日」です。
2026年はこの条件が成立します。
| 日付 | 曜日 | 区分 |
|---|---|---|
| 9/21 | 月 | 敬老の日 |
| 9/22 | 火 | 国民の休日 |
| 9/23 | 水 | 秋分の日 |
敬老の日は9月の第3月曜日、秋分の日は天文計算で決まるため、両者の間隔は年によって変わります。2026年はたまたま1日だけ空き、そこが休日になりました。
結果として9/19(土)から9/23(水)までの5連休が生まれます。
祝日データの一次情報は内閣府が公開しているCSVです。
なぜ抜けたのか
祝日テーブルを確認したところ、振替休日は正しく入っていました。第3条第2項です。
「国民の祝日」が日曜日に当たるときは、その日後においてその日に最も近い「国民の祝日」でない日を休日とする。
つまり第2項は実装されていて、第3項だけが抜けていました。
祝日を計算で生成しようとすると、こうなりがちです。「第3月曜日」「春分・秋分の計算式」「日曜と重なったら翌日」までは意識しますが、第3項の条件は発生頻度が低いため実装が漏れます。実際、2025年から2027年の3年間でこの条件に該当するのは9/22の1日だけでした。
対策としては単純で、内閣府のCSVをそのまま取り込むことです。計算式を自前で書かない。法改正や特例で祝日は動くため、そもそも計算で持つべきデータではありません。
影響:どこがズレるのか
株主優待のクロス取引では、権利を取るために「権利付き最終日」までにポジションを作る必要があります。この日付は次のルールで決まります。
権利付き最終日 = 実効基準日の2営業日前
権利落ち日 = 権利付き最終日の翌営業日
実効基準日とは、権利確定日が非営業日ならその日以前の直近営業日まで繰り上げた日付です。
つまり営業日を遡る計算なので、途中に休日があれば結果がズレます。
月末確定では表面化しない
まず、多くの銘柄が該当する月末確定のケースを見ます。
9/30(水)確定 → 実効基準日 9/30
1営業日前: 9/29(火)
2営業日前: 9/28(月)
→ 権利付き最終日 9/28(月)、権利落ち日 9/29(火)
9/22には到達しません。したがって9/22が営業日として扱われていても結果は変わりません。
これが厄介でした。「シルバーウィークを跨ぐケース」としてこのテストを書いたつもりでいましたが、実際には何も検証できていませんでした。
特定日確定で表面化する
20日確定や25日確定といった、月末以外に権利確定日を持つ銘柄があります。こちらでは9/22を通過します。
9月後半の営業日は次の通りです。9/19〜9/23が5連休なので、営業日は5日しかありません。
9/24(木) 9/25(金) 9/28(月) 9/29(火) 9/30(水)
この状態で25日確定を計算します。
9/25(金)確定 → 実効基準日 9/25
1営業日前: 9/24(木)
2営業日前: 9/18(金) ← 9/23・9/22・9/21と土日を飛ばす
→ 権利付き最終日 9/18(金)
ところが9/22が営業日扱いだと、遡りが9/22で止まります。
| 確定日 | 正しい権利付き最終日 | 9/22欠落時 | ズレ |
|---|---|---|---|
| 9/25(金) | 9/18(金) | 9/22(火) | 4日 |
| 9/24(木) | 9/17(木) | 9/18(金) | 1日 |
ユーザー視点で言えば、こういうことです。
9/18(金)までにポジションを作らなければ権利が取れないのに、アプリは「9/22(火)で間に合う」と表示する。
その通知を受け取った時点で、すでに権利は取れません。日付を扱うアプリとして、これ以上ない壊れ方でした。
テストがすり抜けた理由
このバグを見つけたあと、まず回帰テストを追加しました。
test('2026/9/30(水)確定 → 最終日9/28(月)、権利落ち9/29(火)', () => {
// 9/21・9/22・9/23の3連続休日をスキップすることを確認する意図
});
テスト名に「3連続休日をスキップ」と書いています。しかしこのテストは、9/22を祝日テーブルから削除しても通ります。前述の通り、月末確定は9/22に到達しないからです。
つまり「シルバーウィーク対応のテストを書いた」という記録だけが残り、実際には何も守れていない状態でした。テストを書いた本人が「これで大丈夫」と誤解するという意味で、テストが無いより危険かもしれません。
境界値テストを書くときは、その境界を実際に通過する入力になっているかを確認する必要があります。名前に境界の名前を書いただけでは通過しません。
対策:落ちることを確認する
テストが本当に穴を塞いでいるかは、壊してみれば分かります。
- 祝日テーブルから
2026-09-22の行を一時的にコメントアウトする - 追加したテストが必ず落ちることを確認する
- コメントアウトを元に戻す
- 全テストが通ることを再確認する
これを実際にやったところ、次の結果になりました。
| テスト | 9/22削除時 |
|---|---|
| 月末確定(9/30) | 通ってしまう |
| 25日確定(9/25) | 落ちる(期待 2026-09-18 / 実際 2026-09-22) |
| 24日確定(9/24) | 落ちる(期待 2026-09-17 / 実際 2026-09-18) |
| 既存の全テスト | 通ってしまう |
期待値と実際値の差分が数字で出たことで、「4日ズレる」が実測として確認できました。
ミューテーションテストの手動版のようなものですが、回帰テストを書いたときに1回だけやる価値はあります。手順3の戻し忘れが最悪の事故になるので、そこだけ注意してください。
テーブル範囲内に他の穴はないか
祝日テーブルの収録範囲は2025年から2027年でした。この範囲で国民の休日が発生するのは9/22の1日だけです。
| 年 | 敬老の日 | 秋分の日 | 間 |
|---|---|---|---|
| 2025 | 9/15(月) | 9/23(火) | 離れている |
| 2026 | 9/21(月) | 9/23(水) | 9/22が国民の休日 |
| 2027 | 9/20(月) | 9/23(木) | 2日空く |
国民の休日が成立するのは、敬老の日(第3月曜)と秋分の日がちょうど1日だけ空いて並ぶ年に限られます。敬老の日は9/15〜9/21、秋分の日は9/22か9/23なので、条件はかなり限定的です。
祝日テーブルの収録範囲を延長するときは、延長した範囲について同じ確認が必要になります。
まとめ
- 祝日は計算で作らず、内閣府CSVをそのまま取り込む。 法改正や特例で動くデータを計算式で持たない
- 回帰テストは「消したら落ちるか」まで確認する。 通ることの確認だけでは、何を守れているか分からない
-
境界値テストは、その境界を実際に通過する入力かを確認する。 テスト名に境界の名前を書いても、通過しなければ意味がない
営業日を遡る計算は、金融に限らず受発注や物流の締切計算でも使われます。祝日データが1日欠けるだけで結果が数日ズレるうえ、入力によっては表面化しないため、気づきにくい種類のバグです。
この記事が同じ穴を踏む人を1人でも減らせれば幸いです。
補足
このバグは、株主優待クロス取引の管理アプリ「クロス手帳」の開発中に見つけたものです。修正は公開済みです。