カレンダーと比較して正しい答えが計算されたろうか。では「1969/8/25」はどうだろうか。
目次
サンプル・プログラム(1)の実行例
サンプル・プログラム
nextsunday.php | サンプル・プログラム(1)本体。 |
nextsunday2.php | サンプル・プログラム(2)本体。 |
pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooCalendar.php | 暦計算クラス pahooCalendar。 暦計算クラスの使い方は「PHPで日出没・月出没・月齢・潮を計算」を参照。include_path が通ったディレクトリに配置すること。 |
解説:次の日曜日を計算する
0103: /**
0104: * 次の日曜日を求める(UNIX TIMEで計算)
0105: * @param int $year, $month, $day基点年月日
0106: * @return int次の日曜日(UNIT TIME)/FALSE:定義域エラー
0107: */
0108: function getNextSunday($year, $month, $day) {
0109: $thisday = @mktime(0, 0, 0, $month, $day, $year);
0110: if ($thisday < 0) return FALSE;
0111: $arr = getdate($thisday);
0112: $ww = $arr['wday']; //指定日の曜日番号
0113:
0114: return $thisday + (7 - $ww) * 24 * 60 * 60;
0115: }
仕組みは簡単。
入力された年月日を関数 mktime で UNIX TIME $thisday に変換する。
mktime は計算に失敗した場合は負数を返すので、$thisday が負数だったらエラーを返す。
次に、関数 getdate を使い、その日の曜日番号を求める。曜日番号は 0=日曜日、1=月曜日‥‥6=土曜日なので、7から曜日番号を減じた結果を秒数に換算し、$thisday に加えれば、次の日曜日の UNIX TIME を計算することができる。
解説:入力エラーチェック
0186: //年月日に分解
0187: $arr = preg_split('/\//', $yyyymmdd);
0188: if (count($arr) == 0) {
0189: $errmsg = '入力は 年/月/日 と指定してください';
0190: } else if (count($arr) != 3) {
0191: $errmsg = '年、月、日のいずれかが入力されていません';
0192: } else if (checkdate($arr[1], $arr[2], $arr[0]) == FALSE) {
0193: $errmsg = '入力した年月日は存在しません';
0194: //次の日曜日を求める
0195: } else {
0196: $sunday = getNextSunday($arr[0], $arr[1], $arr[2]);
0197: if ($sunday == FALSE) {
0198: $errmsg = 'この環境では入力した年月日を計算できません';
0199: } else {
0200: $res = '次の日曜日は ' . date('Y/m/d', $sunday) . ' です';
0201: }
0202: }
ここで分解できなかったり、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のユリウス日版
0155: /**
0156: * グレゴリオ暦の年月日の妥当性を判定(checkdateの拡大版)
0157: * @param int $year 西暦年(A.D.0年以降、A.D.1万年未満)
0158: * @param int $month月
0159: * @param int $day 日
0160: * @return int日数/FALSE:引数の異常
0161: */
0162: function checkdate($year, $month, $day) {
0163: $res = TRUE;
0164:
0165: //整数かどうか
0166: if (!is_int($year) || !is_int($month) || !is_int($day)) {
0167: $res = FALSE;
0168: //西暦年の範囲
0169: } else if ($year < 0 || $year > 9999) {
0170: $res = FALSE;
0171: //月の範囲
0172: } else if ($month < 1 || $month > 12) {
0173: $res = FALSE;
0174: //日の判定
0175: } else if (($day < 1) || ($day > $this->getDaysInMonth($year, $month))) {
0176: $res = FALSE;
0177: }
0178:
0179: return $res;
0180: }
ここで、西暦0年未満、1万年以上はエラーとしている。
ユリウス日にしても、32ビット長整数で計算している限り、西暦1175万4266年にオーバーフローを起こしてしまう。天文学の世界ではあり得ない年数ではないので、エラーチェックをしておく必要がある。
解説:getNextSundayのユリウス日版
0106: /**
0107: * 次の日曜日を求める(JulianDayで計算)
0108: * @param int $year, $month, $day基点年月日
0109: * @return int次の日曜日(UNIT TIME)/FALSE:定義域エラー
0110: */
0111: function getNextSunday($year, $month, $day) {
0112: //pahooCalendarクラス
0113: $pcl = new pahooCalendar();
0114: $pcl->setLanguage('jp');
0115:
0116: $thisday = $pcl->Gregorian2JD($year, $month, $day);
0117:
0118: if ($thisday < 0) return FALSE;
0119: $ww = $pcl->getWeekNumber($year, $month, $day); //指定日の曜日番号
0120:
0121: //オブジェクト解放
0122: $pcl = NULL;
0123:
0124: return $thisday + (7 - $ww);
0125: }
その他の解決策
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対応