問題
月額課金の開始日が与えられたとき,今日の日付をもとに次回の決済日を算出せよ。
- 言語はPHPを推奨する。 (今僕がPHPの業務でこのコード書きたくて困ってるから)
- 日付に伴う時刻はすべて
00:00:00
とする。 - 決済日が今日である場合,来月の決済日を算出する。
-
「月」に
+1
しただけでは「日」が未定義になる場合,その月の最終日を採用する。
例:2016-03-30
が課金開始日で今日が2017-01-31
の場合,次回決済日は2017-02-28
関数シグネチャとテストケース
PHP
<?php
use DateTimeImmutable as DTI;
/**
* @param DTI $recursionBaseDate 月額課金の開始日
* @param DTI $today 今日
* @return DTI 次回の決済日
*/
function getNextPaymentDate(DTI $recursionBaseDate, DTI $today): DTI
{
// ここに実装
}
// 基本的なケース
assert(getNextPaymentDate(new DTI('2016-01-05'), new DTI('2017-12-31')) == new DTI('2018-01-05'));
assert(getNextPaymentDate(new DTI('2016-01-05'), new DTI('2017-01-04')) == new DTI('2017-01-05'));
assert(getNextPaymentDate(new DTI('2016-01-05'), new DTI('2017-01-05')) == new DTI('2017-02-05'));
// 開始日が未来のケース
assert(getNextPaymentDate(new DTI('2017-03-31'), new DTI('2017-01-30')) == new DTI('2017-03-31'));
// 単純計算だと日付が未定義になるケース
assert(getNextPaymentDate(new DTI('2015-01-30'), new DTI('2016-01-31')) == new DTI('2016-02-29'));
assert(getNextPaymentDate(new DTI('2015-01-30'), new DTI('2017-01-31')) == new DTI('2017-02-28'));
assert(getNextPaymentDate(new DTI('2015-01-30'), new DTI('2016-02-01')) == new DTI('2016-02-29'));
assert(getNextPaymentDate(new DTI('2015-01-30'), new DTI('2017-02-01')) == new DTI('2017-02-28'));
解答
前者の改善案のほうがわかりやすくておすすめ
@ngyuki案
function getNextPaymentDate(DTI $recursionBaseDate, DTI $today): DTI
{
// 決済開始日が未来の場合はその日を返す
if ($today < $recursionBaseDate) {
return $recursionBaseDate;
}
// 決済開始日の日付
$d = (int)$recursionBaseDate->format('d');
// 今日の年月
$y = (int)$today->format('Y');
$m = (int)$today->format('m');
// 「今日の日」が「決済開始日の日」以降なら翌月
if ((int)$today->format('d') >= $d) {
$m++;
if ($m > 12) {
$m = 1;
$y++;
}
}
// 決済日の月の日数を求める
$days = (int)(new DateTimeImmutable())->setDate($y, $m, 1)->format('t');
// 決済日の日が月の日数を超えているなら月の最終日
if ($d > $days) {
$d = $days;
}
return (new DateTimeImmutable())->setTime(0, 0, 0)->setDate($y, $m, $d);
}
@mpyw案
function getNextPaymentDate(DTI $recursionBaseDate, DTI $today): DTI
{
// 決済開始日が未来の場合はその日を返す
if ($today < $recursionBaseDate) {
return $recursionBaseDate;
}
// 「今月の決済開始日と同じ日」を求める,但し月がズレてしまったら日が足りないと見なして今月の最終日を採用する
$same = $today->setDate($today->format('Y'), $today->format('m'), $recursionBaseDate->format('d'));
if ($same->format('Y-m') !== $today->format('Y-m')) {
$same = $today->modify('last day of');
}
// 「今月の決済開始日と同じ日」がまだ過ぎていなければそれを返す
if ($today < $same) {
return $same;
}
// 「今月の決済開始日と同じ日」の1ヶ月後と,次の月の最終日を求める
$next1 = $same->modify('next month');
$next2 = $same->modify('last day of next month');
// 次の月をスルーしていたら日が足りないと見なして次の月の最終日を返し,そうでなければ計算上の1ヶ月後を返す
return $next1->format('Y-m') === $next2->format('Y-m') ? $next1 : $next2;
}
誤りや改善案があればコメント欄にて