締め日・支払日の計算

請求日・締め日・支払サイトから支払日を出し、土日祝なら前後の営業日に寄せる。祝日は振替休日・国民の休日まで法律どおりに数える。

「1か月」などの定義日付とタイムゾーン

言語ごとの結果

正解表 18件のうち、正解と一致した件数です。

PHPPHP 8.4.26

よくある書き方15 / 18

直した書き方18 / 18

PythonPython 3.14.7

よくある書き方2 / 18

直した書き方18 / 18

GoGo 1.27.2

よくある書き方16 / 18

直した書き方18 / 18

JavaScriptNode 24.15.0

よくある書き方15 / 18

直した書き方18 / 18

JavaJava 21.0.12

よくある書き方17 / 18

直した書き方18 / 18

C#.NET 10.0.402

よくある書き方18 / 18

直した書き方18 / 18

RustRust 1.99.0

よくある書き方17 / 18

直した書き方18 / 18

RubyRuby 4.0.7

よくある書き方18 / 18

直した書き方18 / 18

KotlinKotlin 2.4.21

よくある書き方17 / 18

直した書き方18 / 18

SwiftSwift 6.3.3

よくある書き方18 / 18

直した書き方18 / 18

なぜ割れたのか

よくある書き方が落ちたケースを、原因ごとにまとめた解説です(3本)。書いてある「落ちた言語」が今回の検証結果と一致することを確かめてから載せています。

「1か月」などの定義

1月31日締めの「翌月同日払い」が、3月3日になった

対象のケースc006c007 落ちた言語PHPPythonGoJavaScript

1月31日(月末)締め・翌月同日払いの支払日は、2月に31日がないので 2月28日(うるう年は29日)です。10言語中、Go・JavaScript・PHP の3言語が 3月3日(2024年の c007 は3月2日)を返しました(c006・c007)。

残りの7言語は、支払日そのものは正しく2月28日(c007 は2月29日)を出しました。ただし Python は、曜日の取り違え(別の解説)のために、この2件でも落ちています。

なぜずれたのか

3言語とも、日付ライブラリの「1か月足す」をそのまま使いました。

言語書き方2025年1月31日 + 1か月
Got.AddDate(0, 1, 0)2025-03-03
JavaScriptd.setMonth(d.getMonth() + 1)2025-03-03
PHP$d->modify('+1 month')2025-03-03

どれも、いったん「2月31日」を作り、2月にない3日分を3月に繰り越します。これも1つの決め方ですが、この仕様(元のツールも同じ)は「その月に同じ日がなければ末日」です。

PHP の modify('last day of next month') は2月28日を正しく返します。同じ PHP の中でも、「+1 month」と「last day of next month」で月末の扱いが違います。

正しく2月28日を出した言語

同じ「1か月足す」でも、月末で止める言語があります。

言語書き方2025年1月31日 + 1か月
Java・KotlinplusMonths(1)2025-02-28
C#AddMonths(1)2025-02-28
Rubyclose >> 12025-02-28
SwiftCalendar の date(byAdding: .month, …)2025-02-28

Python の datetime には月を足す関数がないので、翌月の日数を calendar.monthrange で調べて小さい方を取る書き方をしました。Rust も標準ライブラリに日付が無いので、年・月・日の整数で計算し、日と翌月の日数の小さい方を取りました。どちらも2月28日になります。

どう直したか

「翌月の末日」を先に求め、締め日の日がそれより大きければ末日にする関数を書きました。

function addMonthsClamp(d, k) {
  const last = new Date(d.getFullYear(), d.getMonth() + k + 1, 0);
  return new Date(last.getFullYear(), last.getMonth(), Math.min(d.getDate(), last.getDate()));
}

日付とタイムゾーン

Python だけ、曜日がすべて1日ずれた

対象のケースc001c002c003c004c005c008c009c010c011c012c013c014c015c016 落ちた言語PythonJavaKotlin

2025年6月30日(c016)は 月曜 です。Python は日付そのものは正しく出したのに、曜日を 日曜 と返しました。曜日を出すケースは18件中16件で、Python はそのすべてで1日ずれました。

10言語のうち、曜日がずれたのは Python だけです。ほかに Java と Kotlin が、支払日が日曜になる1件(c001)だけエラーで止まりました。残りの7言語(PHP・Go・JavaScript・C#・Rust・Ruby・Swift)は、この解説のケースをすべて正解しています。

なぜずれたのか

曜日を表す番号の振り方が、言語と関数で違います。

言語関数日曜月曜土曜
JavaScriptgetDay()016
GoWeekday()016
PHPformat('w')016
C#DayOfWeek016
Rubywday016
SwiftCalendar の .weekday127
Java・KotlingetDayOfWeek().getValue()(Kotlin は dayOfWeek.value)716
Pythonweekday()605
Pythonisoweekday()716

どの言語も、曜日の表は「日曜始まり」の "日月火水木金土" を使いました。

  • Python: 表を weekday()(月曜が 0)で引いたので、月曜が「日」、火曜が「月」……と1つずつずれました。
  • Java・Kotlin: 番号は月曜が 1、日曜が 7 です。月曜〜土曜はたまたま表と合います。ところが日曜だけは7番目が無いので、StringIndexOutOfBoundsException で止まりました(c001)。表示する曜日が日曜になるのは、このケースだけでした。
  • JavaScript・Go・PHP・C#・Ruby: 日曜が 0 なので、日曜始まりの表とそのまま合い、正解しました。
  • Swift: 日曜が 1 なので、1を引いて使い、正解しました。
  • Rust: 標準ライブラリに日付が無いので、曜日を自分で計算して表の並びに合わせ、正解しました。

一方、同じ Python の中で、土日の判定 d.weekday() >= 5 は正しく動いていました。営業日への寄せ方(支払日)は合っていて、表示する曜日だけが違う、という見つけにくい形です。

どう直したか

Python は、表を weekday() の並びに合わせて、月曜始まりにしました。

WEEK = "月火水木金土日"  # date.weekday() は月曜 = 0、日曜 = 6

日付とタイムゾーン

2025年2月29日を、JavaScript と PHP は3月1日として計算した

対象のケースc017 落ちた言語PHPJavaScriptRust

2025年は平年なので、2月29日は存在しません。正解は invalid_date(エラー)です(c017)。

10言語中7言語(Python・Go・Java・C#・Ruby・Kotlin・Swift)は正しくエラーにしました。JavaScript と PHP は 3月1日 として読み、支払日まで計算して返しました。Rust は「2月29日」のまま計算を続けました。

なぜ通ってしまったのか

言語書き方"2025-02-29" を読むと
Pythondatetime.strptime(s, "%Y-%m-%d")ValueError(エラー)
Gotime.Parse("2006-01-02", s)day out of range(エラー)
Java・KotlinLocalDate.parseDateTimeParseException(エラー)
C#DateOnly.TryParseExact読めない(false)
RubyDate.strptime(s, "%Y-%m-%d")Date::Error(エラー)
SwiftDateFormatter読めない(nil)
JavaScriptnew Date(s + "T00:00:00")2025年3月1日(エラーなし)
PHPDateTimeImmutable::createFromFormat('!Y-m-d', s)2025年3月1日(警告だけ)
Rust- で分けて整数にする確かめないので、2月29日のまま

正しく弾いた7言語は、読めなかったときに invalid_date を返す処理を書いていたので、正解しました。

JavaScript の Date は、存在しない日付を翌月に繰り越します。isNaN(d) で確かめても、3月1日という正しい日付なので通ります。締め日 3月31日・支払日 4月30日と答えました。

PHP の createFromFormat も繰り越しますが、DateTimeImmutable::getLastErrors() に「The parsed date was invalid」という警告を残します。戻り値が false かどうかだけを見ていると、この警告に気づけません。答えは JavaScript と同じです。

Rust は標準ライブラリに日付が無いので、"2025-02-29" を - で分けて年・月・日の整数として読み、確かめないまま計算を続けました。2月29日を2月の日付として扱い、締め日 2月28日・支払日 3月31日と答えています。

どう直したか

  • JavaScript: 年・月・日を数に分けて Date に入れ、できた日付の月と日が入力と同じかを確かめた
  • PHP: getLastErrors() の警告の数も見て、1件でもあれば invalid_date にした

正解表と、よくある書き方の答え

正解は実装より先に決めて固定しています。右の欄は、よくある書き方の答えを言語ごとにまとめたものです(赤い値は実際に返した間違った答え)。

ケース入力正解よくある書き方の答え(10言語)
c001基本・月末締め翌月末払い なぜ? 解説を読む
date
2025-10-15
closing
end
site
next-end
adjust
none
closing
2025-10-31
payment
2025-11-30
adjusted
2025-11-30
weekday
日
  • Pythonweekday"土"
  • Java・Kotlinエラー
  • PHP・Go・JavaScript・C#・Rust・Ruby・Swift✓ 正解
c002支払日が日曜・次の営業日へ 11月30日は日曜 → 12月1日(月) なぜ? 解説を読む
date
2025-10-15
closing
end
site
next-end
adjust
next
closing
2025-10-31
payment
2025-11-30
adjusted
2025-12-01
weekday
月
  • Pythonweekday"日"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c003支払日が日曜・前の営業日へ 11月30日は日曜 → 11月28日(金) なぜ? 解説を読む
date
2025-10-15
closing
end
site
next-end
adjust
prev
closing
2025-10-31
payment
2025-11-30
adjusted
2025-11-28
weekday
金
  • Pythonweekday"木"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c00420日締め・請求日が締め日を過ぎている 10月25日は20日を過ぎているので、締め日は11月20日 なぜ? 解説を読む
date
2025-10-25
closing
20
site
next-end
adjust
none
closing
2025-11-20
payment
2025-12-31
adjusted
2025-12-31
weekday
水
  • Pythonweekday"火"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c005請求日が締め日ちょうど 25日以下なので当月の25日に締める なぜ? 解説を読む
date
2025-10-25
closing
25
site
this-end
adjust
none
closing
2025-10-25
payment
2025-10-31
adjusted
2025-10-31
weekday
金
  • Pythonweekday"木"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c006翌月同日払い・1月31日締め 2月に31日はないので2月28日。3月3日に繰り越してはいけない なぜ? 解説を読む
date
2025-01-10
closing
end
site
next-same
adjust
none
closing
2025-01-31
payment
2025-02-28
adjusted
2025-02-28
weekday
金
  • PHP・Go・JavaScriptpayment"2025-03-03"adjusted"2025-03-03"weekday"月"
  • Pythonweekday"木"
  • Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c007翌月同日払い・うるう年 2024年2月は29日まで なぜ? 解説を読む
date
2024-01-10
closing
end
site
next-same
adjust
none
closing
2024-01-31
payment
2024-02-29
adjusted
2024-02-29
weekday
木
  • PHP・Go・JavaScriptpayment"2024-03-02"adjusted"2024-03-02"weekday"土"
  • Pythonweekday"水"
  • Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c008締め日が年をまたぐ 締め日は翌年1月20日、支払日は2月28日(土)→ 2月27日(金) なぜ? 解説を読む
date
2025-12-21
closing
20
site
next-end
adjust
prev
closing
2026-01-20
payment
2026-02-28
adjusted
2026-02-27
weekday
金
  • Pythonweekday"木"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c009翌々月末・年をまたいで土曜 2026年1月31日は土曜 → 2月2日(月) なぜ? 解説を読む
date
2025-11-30
closing
end
site
next2-end
adjust
next
closing
2025-11-30
payment
2026-01-31
adjusted
2026-02-02
weekday
月
  • Pythonweekday"日"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c010振替休日(昭和の日が日曜) 2029年4月29日は日曜 → 4月30日(月)は振替休日 → 5月1日(火) なぜ? 解説を読む
date
2029-04-10
closing
end
site
this-end
adjust
next
closing
2029-04-30
payment
2029-04-30
adjusted
2029-05-01
weekday
火
  • Pythonweekday"月"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c011振替休日・前の営業日へ 4月30日(月・振替休日)→ 4月29日(日・祝日)・28日(土)を飛ばして 4月27日(金) なぜ? 解説を読む
date
2029-04-10
closing
end
site
this-end
adjust
prev
closing
2029-04-30
payment
2029-04-30
adjusted
2029-04-27
weekday
金
  • Pythonweekday"木"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c012大型連休の振替休日 2026年5月5日(火・祝)→ 5月3日が日曜なので、4日・5日の祝日を飛ばした 6日(水)が振替休日 → 5月7日(木) なぜ? 解説を読む
date
2026-04-03
closing
5
site
next-same
adjust
next
closing
2026-04-05
payment
2026-05-05
adjusted
2026-05-07
weekday
木
  • Pythonweekday"水"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c013国民の休日(シルバーウィーク) 2026年9月20日(日)→ 21日(敬老の日)・22日(国民の休日)・23日(秋分の日)→ 24日(木) なぜ? 解説を読む
date
2026-08-15
closing
20
site
next-same
adjust
next
closing
2026-08-20
payment
2026-09-20
adjusted
2026-09-24
weekday
木
  • Pythonweekday"水"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c014春分の日(計算で決まる祝日) 2026年の春分日は3月20日(金)→ 21日(土)・22日(日)→ 23日(月) なぜ? 解説を読む
date
2026-02-10
closing
20
site
next-same
adjust
next
closing
2026-02-20
payment
2026-03-20
adjusted
2026-03-23
weekday
月
  • Pythonweekday"日"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c0152020年の特例・海の日が移動 2020年7月20日は第3月曜だが、海の日は7月23日に移ったので営業日 なぜ? 解説を読む
date
2020-06-15
closing
20
site
next-same
adjust
next
closing
2020-06-20
payment
2020-07-20
adjusted
2020-07-20
weekday
月
  • Pythonweekday"日"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c016曜日(日曜始まりと月曜始まり) 6月30日は月曜。曜日の番号の振り方(日曜=0 か月曜=0 か)で取り違えやすい なぜ? 解説を読む
date
2025-06-02
closing
5
site
this-end
adjust
none
closing
2025-06-05
payment
2025-06-30
adjusted
2025-06-30
weekday
月
  • Pythonweekday"日"
  • PHP・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift✓ 正解
c017存在しない日付・平年の2月29日 3月1日に繰り越して計算してはいけない なぜ? 解説を読む
date
2025-02-29
closing
end
site
next-end
adjust
none
error
invalid_date
  • PHP・JavaScript・Rusterrornull
  • Python・Go・Java・C#・Ruby・Kotlin・Swift✓ 正解
c018範囲外の請求日
date
2031-01-10
closing
end
site
next-end
adjust
none
error
out_of_range

✓10言語すべて正解

コード

「違い」では、よくある書き方から直した書き方へ、どの行をどう変えたかを示しています(赤が消した行、緑が足した行、濃い色が行の中で変わった部分)。計算の部分だけを載せ、入出力を受け渡す部分は除いています。

Payment.php+12 −3よくある書き方(d08d506)→ 直した書き方

@@ 変更前 3行目〜 / 変更後 3行目〜 @@
33
44 /**
55 * 締め日・支払日の計算。
6− * v1: DateTimeImmutable で計算する素直な書き方(月の加算は modify('+1 month')、月末は 'last day of …')。
6+ * v2: 「翌月の同じ日」は翌月の末日を超えないように自分で決める(modify('+1 month') は 1月31日を 3月3日にする)。
7+ * 請求日は createFromFormat の警告を見て、繰り越された日付(2025-02-29 → 3月1日)を invalid_date にする。
78 */
89 const WEEK = ['日', '月', '火', '水', '木', '金', '土'];
910
@@ 変更前 57行目〜 / 変更後 58行目〜 @@
5758 return (int) $d->format('N') < 6 && !isset(holidays((int) $d->format('Y'))[$d->format('Y-m-d')]);
5859 }
5960
61+/** 翌月の同じ日。その月に同じ日がなければ末日 */
62+function sameDayNextMonth(DateTimeImmutable $d): DateTimeImmutable
63+{
64+ $next = $d->modify('first day of next month');
65+ return $next->setDate((int) $next->format('Y'), (int) $next->format('n'), min((int) $d->format('j'), (int) $next->format('t')));
66+}
67+
6068 /** @return array<string, string> */
6169 function calcPayment(string $date, string $closing, string $site, string $adjust): array
6270 {
6371 $base = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
64− if ($base === false) {
72+ $errors = DateTimeImmutable::getLastErrors();
73+ if ($base === false || ($errors !== false && $errors['warning_count'] > 0)) {
6574 return ['error' => 'invalid_date'];
6675 }
6776 $year = (int) $base->format('Y');
@@ 変更前 81行目〜 / 変更後 90行目〜 @@
8190
8291 $pay = match ($site) {
8392 'this-end' => $close->modify('last day of this month'),
84− 'next-same' => $close->modify('+1 month'),
93+ 'next-same' => sameDayNextMonth($close),
8594 'next-end' => $close->modify('last day of next month'),
8695 default => $close->modify('last day of +2 month'),
8796 };

仕様

入力

項目型説明
date文字列 "YYYY-MM-DD"請求日
closing文字列締め日。"end"(月末)・"5"・"10"・"15"・"20"・"25"
site文字列支払サイト。"this-end"(当月末)・"next-same"(翌月の同じ日)・"next-end"(翌月末)・"next2-end"(翌々月末)
adjust文字列営業日への寄せ方。"none"(寄せない)・"prev"(前の営業日)・"next"(次の営業日)

出力

成功したとき:

項目型定義
closing文字列 "YYYY-MM-DD"締め日
payment文字列 "YYYY-MM-DD"支払サイトから決まる支払日(営業日に寄せる前)
adjusted文字列 "YYYY-MM-DD"営業日に寄せた後の支払日(adjust が "none" なら payment と同じ)
weekday文字列adjusted の曜日。"日"・"月"・"火"・"水"・"木"・"金"・"土" のいずれか

判定できないとき: {"error": "invalid_date"}(存在しない日付)または {"error": "out_of_range"}(請求日が 2020-01-01〜2030-12-31 の外)。

計算

締め日

  • closing が "end": 請求日の月の末日
  • それ以外(日 D): 請求日の日が D 以下なら、請求日の月の D 日。D を過ぎていれば、翌月の D 日
  • D は 25 以下なので、どの月にも D 日がある

支払日(payment)

締め日の年月を起点にする。

site支払日
this-end締め日の月の末日
next-same締め日の翌月の、締め日と同じ日。その月にその日がなければ、その月の末日(1月31日 → 2月28日、うるう年は29日)
next-end締め日の翌月の末日
next2-end締め日の翌々月の末日

営業日への寄せ(adjusted)

  • 営業日 = 土曜・日曜・休日(下記)のいずれでもない日
  • payment が営業日ならそのまま。営業日でなければ、prev は1日ずつ前へ、next は1日ずつ後へ進め、最初に来る営業日にする
  • 年末年始(12月29日〜1月3日など)は、祝日でなければ営業日として扱う(元ツールと同じ。会社ごとの休業日は扱わない)

休日

「国民の祝日に関する法律」に従う。対象は 2020〜2031 年(請求日の範囲から支払日が取りうる年)。

  1. 国民の祝日
名前日
元日1月1日
成人の日1月の第2月曜日
建国記念の日2月11日
天皇誕生日2月23日
春分の日3月の「春分日」(下記)
昭和の日4月29日
憲法記念日・みどりの日・こどもの日5月3日・4日・5日
海の日7月の第3月曜日(2020年は7月23日、2021年は7月22日)
山の日8月11日(2020年は8月10日、2021年は8月8日)
敬老の日9月の第3月曜日
秋分の日9月の「秋分日」(下記)
スポーツの日10月の第2月曜日(2020年は7月24日、2021年は7月23日)
文化の日・勤労感謝の日11月3日・11月23日

2020年・2021年の特例は、東京オリンピック・パラリンピックに合わせた法改正による。

  1. 春分日・秋分日: 国立天文台の近似式で求める(Y は西暦)。2020〜2027年は官報で公表された日と一致することを確かめた。 - 春分日 = ⌊20.8431 + 0.242194 × (Y − 1980) − ⌊(Y − 1980) / 4⌋⌋ - 秋分日 = ⌊23.2488 + 0.242194 × (Y − 1980) − ⌊(Y − 1980) / 4⌋⌋
  2. 振替休日: 国民の祝日が日曜日に当たるときは、その日の後で最も近い「国民の祝日でない日」を休日にする(5月3日が日曜なら、4日・5日を飛ばして6日)
  3. 国民の休日: 前日と翌日がどちらも国民の祝日である日(国民の祝日でない日に限る)は休日にする。振替休日は「国民の祝日」に含めない

日付の扱い

  • 日付はタイムゾーンを持たない暦の日付として扱う。実行環境のタイムゾーンで結果が変わってはいけない
  • 存在しない日付(2月29日の平年、4月31日など)は invalid_date。翌月に繰り越して計算してはいけない

元ツールとの違い

  • 元ツールは請求日の範囲を限らない。本 spec は 2020-01-01〜2030-12-31 に限った(休日の定義を確かめた範囲に合わせる)
  • 元ツールは「国民の休日」の判定で、振替休日も前後の祝日として数える。法律では数えない。2020〜2031年では、この違いで結果が変わる日はない(oracle.py で確かめた)

期待値の作り方

expect は oracle.py で、上の定義を整数の暦(Python の datetime.date)と分数(春分日・秋分日)でそのまま計算して生成した(2026-10-15)。実装ではない。runner は使わない。

休日の定義は、内閣府が公開している「国民の祝日」の一覧(syukujitsu.csv、振替休日・国民の休日を含む)と、公表済みの範囲(2020-01-01〜2027-11-23)で143日すべて一致することを確かめた(2026-10-15)。2028年以降は公表前なので、上の定義(春分日・秋分日は近似式)による。