質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

Q&A

解決済

2回答

1600閲覧

DateTimeクラスで指定の月末営業日を取得するための利用する再帰関数で無限ループが発生します

pegy

総合スコア245

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

0グッド

0クリップ

投稿2021/09/12 14:56

編集2021/09/12 14:57

実装したいこと

  1. $yyyy、月$mを指定することで特定の西暦の月について、最終営業日を取得したいと考えております。
  2. ここでの最終営業日とは、「月末に最も近い日」&&「土日を含まず」「&& $holidaysの休暇リストも含まない」条件を満たすものとします。
  3. DateTimeクラスを利用することでこれを実装しようと思っています。

【開発環境】
PHPのバージョン7.4.1

発生している問題

上記を実装するために、以下の通りコーディングを致しました。今回お尋ねしたいのは$results = exclude_holiday($get_date,$holidays);//hereで利用している自己定義の再帰関数です。
上記の実装の過程で一つ稀なケースが存在しエラーが発生することがわかりました。具体的には、prep($holidays);の出力結果の通りなのですが2019-04-202019-04-30が不運にも連続しており、in_arrayで一階検査するだけでmodify('-1 day')するのでは不十分で、その結果を再帰的に検査していくことが必要であることがわかりました。

###ご質問
その前提でexclude_holiday()を定義したのですが、無限ループしてうまく実装することができません。関数内部の再帰的なexclude_holidayの第一引数がおかしいとはわかっているのですが、どのように実装すればよいのかわからずアドバイスをいただけますと幸いです。
もちろん、目的を達成するために全く別のアプローチでの実装等もご指摘はあろうかと思いますが、再帰関数を使いこなせるようになりたいために、再帰処理をベースとする以下の処理でアドバイスを頂けると有難いです。*あくまで希望です。

php

1<?php 2ini_set('display_errors', "On"); 3 4$yyyy = 2019; 5$m = 4; 6 7$url = "https://holidays-jp.github.io/api/v1/{$yyyy}/date.json"; 8$json = file_get_contents($url); 9$json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN');//文字化対応 10$holidays = array_keys(json_decode($json,true)); 11array_push($holidays,"{$yyyy}-01-02");//銀行休業日追加 12array_push($holidays,"{$yyyy}-01-03");//銀行休業日追加 13array_push($holidays,"{$yyyy}-12-31");//銀行休業日追加 14prep($holidays); 15/* 出力結果 16Array 17( 18 [0] => 2019-01-01 19 [1] => 2019-01-14 20 [2] => 2019-02-11 21 [3] => 2019-03-21 22 [4] => 2019-04-29 23 [5] => 2019-04-30 24 [6] => 2019-05-01 25 [7] => 2019-05-02 26 [8] => 2019-05-03 27 [9] => 2019-05-04 28 [10] => 2019-05-05 29 [11] => 2019-05-06 30 [12] => 2019-07-15 31 [13] => 2019-08-11 32 [14] => 2019-08-12 33 [15] => 2019-09-16 34 [16] => 2019-09-23 35 [17] => 2019-10-14 36 [18] => 2019-10-22 37 [19] => 2019-11-03 38 [20] => 2019-11-04 39 [21] => 2019-11-23 40 [22] => 2019-01-02 41 [23] => 2019-01-03 42 [24] => 2019-12-31 43) 44*/ 45 46$last_day = date('Y-m-d', strtotime("last day of {$yyyy}-{$m}")); //月の最終日 47$last_day_obj = new DateTime($last_day ); 48$w = $last_day_obj -> format('l'); 49$w === "Saturday" ? $last_day_obj -> modify('-1 day'):0; 50$w === "Sunday" ? $last_day_obj -> modify('-2 day'):0; 51$get_date = $last_day_obj -> format('Y-m-d'); 52var_dump($get_date); 53 54$results = exclude_holiday($get_date,$holidays);//here 55var_dump($results); 56 57function exclude_holiday($get_date_arg,$holidays_arg){ 58 if (in_array($get_date_arg,$holidays_arg)){ 59 $back_date = $get_date_arg->modify('-1 day'); 60 } 61 exclude_holiday($back_date,$holidays_arg); 62 return $back_date; 63} 64 65function prep($ary){ 66 echo "<pre>"; 67 print_r($ary); 68 echo "</pre>"; 69}

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

関数exclude_holidayにはDateTimeオブジェクトを渡さなくてはいけない、という問題は自力で解決できたようなので(その部分も含めて検証していました)、関数の内容について、

PHP

1function exclude_holiday($date_obj_arg, $holidays_arg) 2{ 3 if (in_array($date_obj_arg->format('Y-m-d'), $holidays_arg)) { 4 $date_obj_arg->modify('-1 day'); 5 exclude_holiday($date_obj_arg, $holidays_arg); 6 } 7 return; 8} 9
  • modify()は、対象のDateTimeオブジェクト自体を書き替えるので、返り値を=で代入する必要はない。
  • 再帰呼び出しが必要なのは、if文の条件が成立したときだけなので、exclude_holidayの呼び出しはif文の中に入れる。
  • 最初に実引数に指定した$last_day_objが、(再帰呼び出しにおいても)直接書き替えられるので、返り値を返す必要はない。関数終了後、$result = $last_day_obj->format('Y-m-d');とすればいい。

補足

再帰関数を使いこなせるようになりたいために、再帰処理をベースとする以下の処理でアドバイスを頂けると有難いです。

ということだったので再帰関数を使ったが、返り値を使わないのでわざわざ再帰させる意味がない。実際に書くとしたらwhileループを使う。たとえば、

PHP

1function exclude_holiday($date_obj_arg, $holidays_arg) 2{ 3 while (in_array($date_obj_arg->format('Y-m-d'), $holidays_arg)) { 4 $date_obj_arg->modify('-1 day'); 5 } 6 return; 7}

とすれば同じ結果が得られる。すなわち、呼び出すときの実引数に指定した$last_day_objが指すDateTimeオブジェクトの内容が、祝日の間はwhileループで直接1日ずつ前に戻る。

ところで、「月末の月曜日が祝日(あるいは月末から直近の月曜日までが連続して祝日)だと、最終営業日を得るにはexclude_holidayの結果からさらに遡らないといけない」ことにそろそろ気がついたのでは。

実際、2019-04-28は日曜日なので、実際の最終営業日はさらに遡った2019-04-26になる
if文(あるいはwhile文)の継続条件を「祝日のリストに含まれるか、または、土曜か日曜日に当たる」に変更する必要があるのでは。それなら、土日を遡る処理を別に書く必要もなくなる。

投稿2021/09/13 01:46

編集2021/09/13 19:58
Daregada

総合スコア11990

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

pegy

2021/09/13 03:37

ありがとうございます。 >modify()は、対象のDateTimeオブジェクト自体を書き替えるので、返り値を=で代入する必要はない。 理解いたしました、ご指摘の通りです。 >最初に実引数に指定した$last_day_objが、(再帰呼び出しにおいても)直接書き替えられるので、返り値を返す必要はない。関数終了後、$result = $last_day_obj->format('Y-m-d');とすればいい。 理解いたしました、ご指摘の通りです。 >再帰呼び出しが必要なのは、if文の条件が成立したときだけなので、exclude_holidayの呼び出しはif文の中に入れる 中に入れること自体は理解できるのですが、変更した物自体も返り値としてreturnしなければいけないのではないでしょうか?実際に以下のコードではNULLが返ってきてしまうことを確認いたしました。または引数自体のオブジェクトを変化させるため参照渡しなのかとfunction exclude_holiday(&$date_obj_arg,$holidays_arg)とも想像してみたのですが、適切に実行することができませんでした。。 function exclude_holiday($date_obj_arg,$holidays_arg){ if (in_array($date_obj_arg -> format('Y-m-d'),$holidays_arg)){ $date_obj_arg ->modify('-1 day'); exclude_holiday($date_obj_arg,$holidays_arg); } return; }
Daregada

2021/09/13 04:38 編集

「返り値を使わない」んですよ。 PHPのオブジェクトは元々参照型のようなもの(オブジェクト自身ではなく、オブジェクトIDを保持する)なので、&を付ける必要はありません。 再帰呼び出しを重ねている間、仮引数 $date_obj_arg が参照するオブジェクトは一貫して、最初の呼び出しで実引数に指定された$last_day_objの指すDateTime型のオブジェクトそのものです。
pegy

2021/09/13 08:22

ありがとうございます。 >「返り値を使わない」んですよ。 とはexclude_holiday()の2回目以降の第1引数として$last_day_objの意でしょうか? 重ねて失礼いたします。
Daregada

2021/09/13 09:10

関数における返り値とは、return文で呼び出し元に返される値のことです。引数で与えられるオブジェクトIDのオブジェクトを変更するだけで目的を達成できるので、return文に返り値を書かない、つまりnullを返しますがそれを使わないということです。
pegy

2021/09/13 15:40

本文への追記ありがとうございます。再帰処理に固執していましたが、while文でなるほどと思わされました。仰る通り、2019年4月の処理をしていたら、祝日をよけても土日ぶぶち当たりました・・・! そこもまさにロジックの再構築をしておりました。
guest

0

return に行き着くことがないので、再帰関数が終了することはありません。
再帰関数には必ず終了する条件が必要です。

例えば以下のようなコードは終わりません。

php

1function countup($count){ 2 countup($count + 1); 3 return $count; 4}

以下のように終了する条件が必要です。

php

1function countup($count){ 2 if ($count > 10) { 3 return $count; 4 } 5 return countup($count + 1); 6}

投稿2021/09/12 15:12

mather

総合スコア6759

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

pegy

2021/09/13 01:20 編集

コメントありがとうございます。抜けることができるように以下のように指定したのですが、4/29を返してきてしまいます。また本文中のコードでmodify()メソッドが利用できるのはDateTimeオブジェクトであるため、若干下記で修正しております。 $last_day = date('Y-m-d', strtotime("last day of {$yyyy}-{$m}")); //月の最終日 $last_day_obj = new DateTime($last_day ); $w = $last_day_obj -> format('l'); $w === "Saturday" ? $last_day_obj -> modify('-1 day'):0; $w === "Sunday" ? $last_day_obj -> modify('-2 day'):0; $get_date = $last_day_obj -> format('Y-m-d'); $results = exclude_holiday($last_day_obj,$holidays); var_dump($results); function exclude_holiday($date_obj_arg,$holidays_arg){ if (in_array($date_obj_arg -> format('Y-m-d'),$holidays_arg)){ $date_obj_arg = $date_obj_arg ->modify('-1 day'); }else{ var_dump($date_obj_arg);//ここでは4/28を含むobjを返している return $date_obj_arg; } exclude_holiday($date_obj_arg,$holidays_arg); } Nullが返ってきてしまい、 「var_dump($date_obj_arg);//ここでは4/28を含むobjを返している」までは正しく出力されているのですが。。 重ねて失礼いたします。
mather

2021/09/13 03:05

再帰関数なので、 exclude_holiday => exclude_holiday => exclude_holiday => ... と呼び出されます。 このとき、最後に単純に exclude_holiday(...) と呼び出してしまうと、この関数呼び出しの返り値は return されませんよね? 返り値を使いたいのですから、 return exclude_holiday(...) と書きましょう。
pegy

2021/09/13 03:44

コメントありがとうございます。 最後にfalseである場合、elseの中身が実行されてreturnされるので処理が止まる+その時点の$date_obj_argを返す(つまり、falseになった場合には最後のexclude_holiday()にはたどり着かない)思っていたのですが違うようですね。 少し頭も混乱してきたのですが、これを踏まえるとexclude_holiday()を再帰的に実行すると必ずelse以降も処理されてしまうので、以下のようにしたら、目的通りの結果は得ることはできました。 function exclude_holiday($date_obj_arg,$holidays_arg){ if (in_array($date_obj_arg -> format('Y-m-d'),$holidays_arg)){ $date_obj_arg ->modify('-1 day'); }else{ return $date_obj_arg; } exclude_holiday($date_obj_arg,$holidays_arg); return $date_obj_arg; } ただ、上記のmather様のコメントも踏まえるとできてはいても適切ではないような・・・ もう少し検証してみます。
mather

2021/09/13 03:51

①exclude_holiday => ②exclude_holiday の二回目の呼び出しで return false; となった場合を考えてみましょう。 この false は②の関数の返り値ですよね。では①の関数の返り値はどうなりますか?
pegy

2021/09/13 08:20

んんん、特に存在しないNULLではないでしょうか? ②exclude_holidayの引数としてだけ使われたと考えています。
mather

2021/09/13 08:27

https://www.php.net/manual/ja/functions.returning-values.php そうですね。nullが返されます。 Daregadaさんの仰るように、「返り値を使わない」のであればそもそもこれでも良いです。 (再帰関数である意味は無いですが) > ②exclude_holidayの引数としてだけ使われたと考えています。 これはどの値のことを言っているんでしょうか?わかりません。
pegy

2021/09/13 15:54

返り値を使わないという考えかたやそもそも、modify()されるたびにオブジェクトの中身が書き換わっているということはある程度理解できたと思います。 本当はここでcloseしてありがとうございました、というところなのですが、正直当初の考え方でNULLが返ってくることに納得がいかず、あれから自分でいろいろコードを書いてみて検証してみました。 ※論理的にまだ理解できない部分があるのは一旦仕方ないので、実際の動作を確認してみました。 ①そもそも返り値が必要ないので(実際には再帰的ではないが)再帰風な処理をするケース class recursive_minus { public $num = 10; public function minus($n){ $this -> num = $this -> num - $n; } } function recursive_func($obj){ if ($obj->num > 6) { $obj->minus(1); recursive_func($obj); } return; } $test = New recursive_minus(); recursive_func($test); echo $test->num;//6 確かに$test オブジェジェクトの中身が書き換わっているので、わざわざ return $obj;をしなくても意図した結果が得られています。
pegy

2021/09/13 16:00

② 当初の間違った考え方に基づきNULLが返ってくるようなケース class recursive_minus { public $num = 10; public function minus($n){ $this -> num = $this -> num - $n; } } function recursive_func($obj){ if ($obj->num > 6) { $obj->minus(1); }else{ return $obj; //6 } recursive_func($obj); return $obj; } $test = New recursive_minus(); recursive_func($test); echo $test->num; //6 ※今にして思えば、再帰処理にしても少し意味不明で申し訳ないのです(最後のreturn $obには絶対に行かないので意味がない1文であったりします)。ただ、それでも6までminusが進むとelseに飛びそこで初めてreturnされる(返り値も本来は必要ないことは今では理解できています)のでこれでも一応6が返ってくるので問題となっていた「NULL」の結果は得られませんでした。。
pegy

2021/09/13 16:02

と、せっかく一日学んだことや試したことを備忘録のように残してしまいましたが、なぜオリジナルのコードでは同じ論理であるはずなのにNULLが返されたのかを記載させていただきました。後になって、同じようなことで頭を悩まし、この記事を見る人がいるとはあまり思えないのですが、後学のために記載をさせて頂きました。 ご迷惑とお手数ばかりおかけして申し訳ございません。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問