2008年01月05日

日付入力のチェックその4

前回、日付入力チェック用のクラスを書いている過程で、mktime()に依存している部分をなんとかしたいと考えていたところ、PHPのカレンダー関数なるものに到達?しました。

mktime()も含めたPHPのdatetime系の関数は、UNIXエポックの縛りがあるため、1970年を基点として前後およそ70年ぐらいまで(PHP5.1.0以前で環境がWindowsの場合は1970より過去は扱えない)しか扱えません。

これは要するに時間(時分秒)までを含めて32ビット整数の範囲で扱おうとしていることによる縛りで、今回の場合のように時間を扱わず年月日だけの場合は、カレンダー関数の方が広範囲の年月日を扱うことができるということです。

そこで早速これを使って書き直そうとしたのですが、いきなり壁?というか仕様だからしょうがない?というか、にぶち当たりました。

この日付入力チェックでは前述したとおり[2005.3.4-2006.10.21]のように範囲指定で入力されることを基本形として想定していますが、これが[2006.10.21-2005.3.4]のように前後の日付の関係が前のものより後ろの方が過去になる場合というのも考慮しています。

mktime()に依存した前のバージョンでは、これらの日付を一旦UNIXタイムスタンプに変換してから比較して、前後関係がおかしい場合(前のものより後ろの方が過去)場合は入替えています。

で、カレンダー関数で書き換える場合は、UNIXタイムスタンプに相当するものとしてユリウス日(PHPの日本語訳マニュアルではユリウス積算日)を使います。

ユリウス日(通日・積算日)というのは、紀元前4713年1月1日からの通算の日付ということらしいのですが、桁数が多くなりすぎるから修正ユリウス日という一定数を引いたものを使ったりとかいろいろあるみたいです。

んで、ここでは単に日付を比較できればいいわけで、あまり難しいことは考えずに、カレンダー関数のGregorianToJD()という日付をユリウス日に変換してくれる関数を使ってみました。
return GregorianToJD($this->getMonth(), $this->getDay(), $this->getYear());

ところが通常の入力値、つまり月が1〜12までで日が1〜31の場合はこれでよいのですが、例えば[2006.13.10]とか[2006.5.35]などの場合にエラーになってしまいます。

mktime()の場合は↑のような不正?な日付も勝手に変換してくれていたわけです。
2006.13.10 -> 2007.1.10
2006.5.35 -> 2006.6.4

このmktime()の機能?はかなり便利で、後述しますが0が入力された場合でもよきに取り計らってくれます。
2006.0.5 -> 2005.12.5
2006.5.0 -> 2005.4.30
2004.3.0 -> 2004.2.29

最後の例のように閏年もきちんと考慮してくれます。

ですが前述のとおりmktime()はUNIXエポックの縛りがあるためあまり使いたくありません。まあここでの市町村合併データベースでは範囲的には全く困らない(今のところ)のですが、2038年以降にはまずいわけで(^^;(あと30年もありますが…)まあとにかく今更mktime()に戻るわけにはいかないのです。

というわけでユリウス日変換の際に不正な日付を受け付けるようなModGregorianToJD()というメソッドをでっちあげることにしました。
function ModGregorianToJD($month, $day, $year)
{
/***** 6桁までに丸める(7桁の数値でエラーが出るため) *****/
$y = (INT)substr($year, 0, 6);
$m = (INT)substr($month, 0, 6);
$d = (INT)substr($day, 0, 6);

/***** 溢れた日数:初期化 *****/
$overDay = 0;

/***** 日が0の場合は先月(月が0,-1になる可能性がある) *****/
if ($d == 0) { $m = $m - 1; }

/***** 月が0,-1の場合は昨年(年が0になる可能性がある) *****/
if ($m <= 0) { $y = $y - 1; }

/***** 月が-1の場合は11月(これ以上戻る可能性はない:YYYY/0/0 => YYYY/11/30) *****/
if ($m < 0) { $m = 11; }

/***** 月が12より大きい場合12で割って切り捨てて年に足す(月の不正値:13月〜) *****/
$y = $y + ($m > 12 ? (INT)(floor($m / 12)) : 0);

/***** 12月に納まるように12の剰余を月に設定(0の場合は12) *****/
$m = ($m % 12 == 0) ? 12 : $m % 12;

/***** 年が0以下(紀元前または西暦0年(存在しない))の場合は強制的に西暦1年に設定 *****/
if ($y <= 0) { $y = 1; }

/***** 設定された年月からグレゴリオ暦での月の最後の日を取得 *****/
$lastDay = cal_days_in_month(CAL_GREGORIAN, $m, $y);

/***** 日が0の場合は上で求めた月の最後の日を日に設定 *****/
if ($d == 0) { $d = $lastDay; }

/***** 日が月の最後の日を超える場合 *****/
if ($d > $lastDay)
{
/***** 元の日から月の最後の日を引いて溢れた日数を取得 *****/
$overDay = $d - $lastDay;

/***** 日に月の最後の日を設定 *****/
$d = $lastDay;
}

/***** ユリウス積算日取得 *****/
$JD = GregorianToJD($m, $d, $y);

/***** 溢れた日数をユリウス積算日に足して返す *****/
$newJD = $JD + $overDay;

return $newJD;
}

基本方針としては元のGregorianToJD()に渡しても大丈夫なように年月日を修正してから変換し、溢れた日数は後から足しているというだけです。

一応0も考慮しています。また閏年はcal_days_in_month()という関数できちんと計算してくれます。

冒頭で入力値を6桁までに限定している部分は、テストしてみたら7桁の数値でGregorianToJDがエラーになった(マニュアルにはBC.4714〜AD.9999となっています)ので、さしあたって6桁にしておきました。ただこの部分はこのメソッド内に書くべきことではないような気もします。

次回はこれを日付入力クラスに組み込みます。
タグ:codeigniter PHP
posted by ciallost at 18:45| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする

日付入力のチェックその3

前回、日付入力チェック用のクラス(の雛型?)を作成しましたが、今回はそれをマトモなものに修正していきます。

まずPeriodクラス,Ymdクラスのコンストラクタに入力チェック?というか整形?用のコードを付け足します。

Periodクラス function __construct($gdate)
$temp = preg_replace("/-+/", "-", $gdate);
$temp = preg_replace("(^-|-$)", "", $temp);

連続する[-]ハイフンを1個にして、行頭または行末のハイフンを削除します。

Ymd(日付抽象)クラス function __construct($date)
$temp = preg_replace("/[^0-9.]/", "", $date);
$temp = preg_replace("/\.+/", ".", $temp);
$temp = preg_replace("/(^\.|\.$)/", "", $temp);

数値と[.]ピリオド以外は削除して、連続ピリオドを1個に、行頭・行末のピリオドを削除します。

次に日付のtoクラスの方ですが、今までのところでは値がセットされていない場合はfromと同じように1を返していましたが、これを次のようなルールで適当な値を返すように書き換えます。

・年の値がない場合は、fromの年
・月の値がない場合は、fromの月があればそれ、なければ12
・日の値がない場合は、fromの日があればそれ、なければ月の最後の日

というようにしたいのですが、いずれにしろtoからfromに問い合わせる必要があるため、まずコンストラクタを書き換えてfromへの参照を保持するようにします。
class dateTo extends Ymd
{
private $from;

function __construct($date, Ymd $from)
{
$this->from = $from;
parent::__construct($date);
}
:
:
}

この変更に伴いPeriodクラスからdateToを生成している部分を書き換えます。
$this->from = new dateFrom($from);
$this->to = new dateTo($to, $this->from);

先にfromのインスタンスを生成して、toを生成するときにはそれを渡します。

尚、前回は端折っていましたが、Ymdクラス(dateFromとdateToの親)でメンバを初期化するinit()を定義し、それぞれの子クラスで実装します。
abstract class Ymd
{
protected $year;
protected $month;
protected $day;

function __construct($date)
{
$temp = preg_replace("/[^0-9.]/", "", $date);
$temp = preg_replace("/\.+/", ".", $temp);
$temp = preg_replace("/(^\.|\.$)/", "", $temp);
$temp = explode('.', $temp, 3);
:
:
$this->init($tempDate);
}

abstract function init(array $date);
:
:
}

dateFromクラス
function init(array $date)
{
list($this->year, $this->month, $this->day) = $date;
}

dateToクラス
function init(array $date)
{
switch (count($date))
{
case 1:
$this->day = $date[0];
break;
case 2:
if ($date[0] <= 1900)
{
list($this->month, $this->day) = $date;
} else {
list($this->year, $this->month) = $date;
}
break;
case 3:
list($this->year, $this->month, $this->day) = $date;
break;
}
}

dateFromの方は単純に配列をlist()でセットしているだけです。この場合値の数が足りない場合は単にセットされないだけです。つまり年月日の順で値がセットされていくということです。

dateToの方はdateFromとはほぼ逆で、値が1個の時はそれを「日」とみなします。値が3個全部揃っている場合はdateFromと同じですが、値が2個の時は1個目の値に閾値を設けて、それより大きな場合は「年月」とし小さい場合は「月日」とみなすことにします。閾値はとりあえず適当に1900としました。

そしてdateToの年月日それぞれのゲッターですが、↑のルールに則って次のように書きました。
function getYear()
{
if ($this->year <> '')
{
return $this->year;
} else {
return $this->from->getYear();
}
}

function getMonth()
{
if ($this->month <> '')
{
return $this->month;
} else {
if ($this->from->month <> '')
{
return $this->from->getMonth();
} else {
return 12;
}
}
}

function getDay()
{
if ($this->day <> '')
{
return $this->day;
} else {
if ($this->from->day <> '')
{
return $this->from->getDay();
} else {
return date('t', mktime(0,0,0,$this->getMonth,1,$this->getYear));
}
}
}

で、これでだんだんと希望のものに近づいてはきているのですが、一番の問題はmktime()に依存している部分で、これをどうにかできないか?と考えて…いやPHPのマニュアルを見ていたところ、カレンダー関数なるものを見つけました。

で、UNIXタイムスタンプの縛りがないということが判明。急遽そっちを使うことにしました。
タグ:codeigniter PHP
posted by ciallost at 01:42| Comment(0) | TrackBack(1) | 日記 | このブログの読者になる | 更新情報をチェックする
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。