カレンダーと比較して正しい答えが計算されたろうか。では「1969/8/25」はどうだろうか。
目次
サンプル・プログラム(1)の実行例
サンプル・プログラム
nextsunday.php | サンプル・プログラム(1)本体。 |
nextsunday2.php | サンプル・プログラム(2)本体。 |
pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooCalendar.php | 暦計算クラス pahooCalendar。 暦計算クラスの使い方は「PHPで日出没・月出没・月齢・潮を計算」を参照。include_path が通ったディレクトリに配置すること。 |
解説:次の日曜日を計算する
103: /**
104: * 次の日曜日を求める(UNIX TIMEで計算)
105: * @param int $year, $month, $day 基点年月日
106: * @return int 次の日曜日(UNIT TIME)/FALSE:定義域エラー
107: */
108: function getNextSunday($year, $month, $day) {
109: $thisday = @mktime(0, 0, 0, $month, $day, $year);
110: if ($thisday < 0) return FALSE;
111: $arr = getdate($thisday);
112: $ww = $arr['wday']; //指定日の曜日番号
113:
114: return $thisday + (7 - $ww) * 24 * 60 * 60;
115: }
仕組みは簡単。
入力された年月日を関数 mktime で UNIX TIME $thisday に変換する。
mktime は計算に失敗した場合は負数を返すので、$thisday が負数だったらエラーを返す。
次に、関数 getdate を使い、その日の曜日番号を求める。曜日番号は 0=日曜日、1=月曜日‥‥6=土曜日なので、7から曜日番号を減じた結果を秒数に換算し、$thisday に加えれば、次の日曜日の UNIX TIME を計算することができる。
解説:入力エラーチェック
186: //年月日に分解
187: $arr = preg_split('/\//', $yyyymmdd);
188: if (count($arr) == 0) {
189: $errmsg = '入力は 年/月/日 と指定してください';
190: } else if (count($arr) != 3) {
191: $errmsg = '年、月、日のいずれかが入力されていません';
192: } else if (checkdate($arr[1], $arr[2], $arr[0]) == FALSE) {
193: $errmsg = '入力した年月日は存在しません';
194: //次の日曜日を求める
195: } else {
196: $sunday = getNextSunday($arr[0], $arr[1], $arr[2]);
197: if ($sunday == FALSE) {
198: $errmsg = 'この環境では入力した年月日を計算できません';
199: } else {
200: $res = '次の日曜日は ' . date('Y/m/d', $sunday) . ' です';
201: }
202: }
ここで分解できなかったり、3つの数字にならないケースはエラーとして、変数 $msg にエラー・メッセージを設定する。
次に、うまく分解できたとしても、2008年2月31日のような異常な入力があるかもしれない。そこで、関数 checkdate を使って年月日の整合性をチェックする。
以上のエラーチェックを通過した場合のみ、ユーザー関数 getNextSunday を適用する。
定義域エラー
これは、 mktime をはじめとする日付関数が、1970年1月1日を起点とする UNIX TIME(UNIX epoch)に起きる問題である。このため、1969年8月25日は負数となってしまう。⇒「国際原子時と協定世界時」
この値は、1970年1月1日を原点として正確に負の数となっていればいいのだが、そうならない環境もある―― mktime は仕様上、負の結果については保証していないので、一律にエラーとして扱う必要がある。
次に未来日を見ていこう。
年月日に「2038年1月1日」を入力して計算してみてほしい。正常に計算されるはずである。
では「2038年1月20日」はどうか――今度はエラーになってしまう。
これは「2038年問題」と呼ばれているエラーで、2038年1月19日にUNIX の time_t型(32bit長整数)が桁あふれを起こすのが原因である。
最近の UNIX/Linux では time_t型が64bitに拡張されており、PHP 5.xであれば64bit長のtime_t型に対応できる。
しかし、サーバOSの種類やPHPのバージョンによって2038年問題が起きるかどうかは不確定であるため、これも一律にエラーとして扱う必要がある。
とくに問題なのが、「2038年1月17日」を入力した場合である。
この日は関数 mktime の定義域範囲内(2038年問題以前)なのだが、次の日曜日を求めるために日数を加算した結果、桁あふれを起こしてしまうのである。その結果、エラーは表示されないものの、異常な結果が表示される。
さらに、実行時エラー対策を施していない環境では、エラー発生箇所の行番号などが表示されてしまう。
悪意のあるアタッカーは、こうしたエラーメッセージを頼りに、スクリプトの脆弱性を突いてくる。このサンプルプログラムほど単純なものでは問題ないが、ある程度の規模のスクリプトになると、こうしたエラーがセキュリティホールになる恐れがある。何らかの対策を講じなければならない。
通日によるカレンダー計算
そこで、秒を基数にするのではなく、日を基数にしたらどうなるだろうか――1年は365、100年は3万6525――これなら16ビット長整数でもおさまる。
サンプル・プログラム(2)の実行例
サンプル・プログラム(2):ユリウス日版
2番目のサンプルプログラムは、ユリウス日を使って「次の日曜日」を計算するものである。ユリウス日の計算などについては、「カレンダー計算 - PHPで祝日を求める」をご覧いただきたい。
解説:checkdateのユリウス日版
ここで、西暦0年未満、1万年以上はエラーとしている。
ユリウス日にしても、32ビット長整数で計算している限り、西暦1175万4266年にオーバーフローを起こしてしまう。天文学の世界ではあり得ない年数ではないので、エラーチェックをしておく必要がある。
解説:getNextSundayのユリウス日版
106: /**
107: * 次の日曜日を求める(JulianDayで計算)
108: * @param int $year, $month, $day 基点年月日
109: * @return int 次の日曜日(UNIT TIME)/FALSE:定義域エラー
110: */
111: function getNextSunday($year, $month, $day) {
112: //pahooCalendarクラス
113: $pcl = new pahooCalendar();
114: $pcl->setLanguage('jp');
115:
116: $thisday = $pcl->Gregorian2JD($year, $month, $day);
117:
118: if ($thisday < 0) return FALSE;
119: $ww = $pcl->getWeekNumber($year, $month, $day); //指定日の曜日番号
120:
121: //オブジェクト解放
122: $pcl = NULL;
123:
124: return $thisday + (7 - $ww);
125: }
その他の解決策
PEAR が利用できる環境であれば、PEAR::Date を使ってカレンダー計算を行うことができる。
参考サイト
- 国際原子時と協定世界時:ぱふぅ家のホームページ
- 日付と時刻:ぱふぅ家のホームページ
- セキュアプログラミング講座-Webアプリケーション編:IPA
- 2038年問題:IT用語辞典 e-Words
- KDDI、「2038年問題」で1346万円を過剰請求:ITmedia
- ユリウス日:理科年表
まず、組み込み関数 mktime や getdate を使って作るのだが、数十年以上昔の日付や数十年以上未来の日付を入力すると、必ずエラーが発生する。これは日付関数の定義域エラーによるものだが、このようなエラーはセキュリティ・ホールに繋がる恐れがあるので、何らかの対策を施す必要がある。
日付に限らず、数学関数などには、かならず引数の定義域がある。入力データのバリデーションだけでなく、定義域をチェックすることも忘れないようにしよう。
(2022年6月27日)FastCGIで正常動作しない不具合を修正
(2022年5月22日)pahooInputData.php分離,pahooCalendarクラス利用,PHP8対応