PHP 5.3.0 の日付処理クラスと関数の追加・変更について

PHP 5.3.0 が公開されたのは結構前ですが、日付関係の処理について、PHP 5.3.0 でクラスや関数の追加・変更がありましたので、気になった部分だけ調べてみました。

新しく追加されたクラスとメソッド

PHP 5.3.0 では、以下のクラスが追加されました。

DateTime クラスには、以下のメソッドが追加されました。

詳細は、PHP マニュアルの PHP 5.2.x から PHP 5.3.x への移行で確認できます。追加されたクラスは、主に、追加されたメソッド(DateTime::add(), DateTime::diff(), DateTime::sub())の引数や返り値として使用されます。

使用例としては、以下の通りです。

<?php
// 1年と3日後を求める
$datetime = new DateTime( '2010-01-11' );
$datetime->add( new DateInterval( 'P1Y3D' ) ); // 1年と3日
echo "1. " . $datetime->format( 'Y-m-d' ) . "\n";

// 500日前を求める
$datetime = new DateTime( '2010-01-11' );
$datetime->sub( new DateInterval( 'P500D' ) ); // 500日
echo "2. " . $datetime->format( 'Y-m-d' ) . "\n";

// 2010-01-11 から 2014-01-11 までの日数を求める
$datetime1 = new DateTime( '2010-01-11' );
$datetime2 = new DateTime( '2014-01-11' );
echo "3. " . $datetime1->diff( $datetime2 )->format( '%a' ) . "\n";;

// 2010-01-01 から 2010-01-31 までを 1週ごとにループ
$start_date = new DateTime( '2010-01-01' );
$end_date   = new DateTime( '2010-01-31' );
$interval   = new DateInterval( 'P7D' ); // 7日間隔
foreach ( new DatePeriod( $start_date, $interval, $end_date ) as $d ) {
	echo "4. " . $d->format( "Y-m-d\n" );
}

結果は以下のようになります。

1. 2011-01-14
2. 2008-08-29
3. 1461
4. 2010-01-01
4. 2010-01-08
4. 2010-01-15
4. 2010-01-22
4. 2010-01-29

DatePeriod を使用すると、一定間隔で日付や時間を進めることができますので、カレンダーやスケジューラのようなアプリケーションの作成には便利かもしれません。

次月を取得する方法

Obtaining the next month in PHP によると、PHP 5.3.0 以降では、"first day of next month" という書き方ができるようになったそうです。
strtotime() や、DateTime クラスで、次月を取得する場合、最初に "next month" を使用することを思い付きますが、1/31 などを指定すると、正しく次の月にならない場合があります。

<?php
$d = new DateTime( '2010-01-31' );
$d->modify( 'next month' );
echo $d->format( 'F' );

結果は、以下のように "March" となります。

March

PHP 5.3.0 からは、"first day of next month" という書き方で、"February" が取得できます。

<?php
$d = new DateTime( '2010-01-31' );
$d->modify( 'first day of next month' );
echo $d->format( 'F' );

PHP 5.3.0 以降では、以下のように "February" が表示されます(PHP 5.2.x では、"March" が表示)。

February

DateTime::modify() が変更後の DateTime を返すようになった

PHP 5.3.0 からは、以下のように DateTime::modify() の後ろにメソッドを続けて書くことができるようになりました(参考: DateTime::modify)。

<?php
$datetime = new DateTime( '2010-01-11' );
echo $datetime->modify( 'last day' )->format( 'Y-m-d' ) . "\n";

// 以下のような書き方も可能です
echo date_create( '2010-01-11' )->modify( 'last day' )->format( 'Y-m-d' ) . "\n";

結果は以下のようになります(PHP 5.2.x ではエラーになります)。

2010-01-31
2010-01-31

指定した年月の第[1-5][日-土]曜日の日付を求める関数を書き直してみる

以前にも、指定した年月の第[1-5][日-土]曜日の日付を求める関数例を紹介したことがあるのですが、今回、strtotime() や DateTime::modify() メソッドで、使用可能なキーワードを調べてみたところ、もう少し簡単な方法を思い付いたので、書いておきたいと思います。
strtotime() を使用した場合は以下のように書くことができます。こちらは、PHP 4.4.9 でも動作することを確認しました。ただし、strtotime() には 2038-01-19 以降の日付は正しく取得できないなどの制限がある場合があります(PHP マニュアル strtotime の注意を参照)。

<?php
/**
 * 指定した年月の第[1-5][日-土]曜日の日付を求める
 * strtotime() 使用版
 */
function calc_day( $year, $month, $week_of_month, $day_of_the_week )
{
	$year_month = sprintf( '%04d-%02d', (int)$year, (int)$month );
	$time = strtotime( (int)$week_of_month . ' '  . $day_of_the_week, strtotime( $year_month ) );

	if ( $time < 0 || $time === FALSE || $year_month !== date( 'Y-m', $time ) ) {
		return FALSE;
	}
	$result = (int)date( "j", $time );
	if ( ! checkdate( $month, $result, $year ) ) {
		return FALSE;
	}
	return $result;
}

DateTime クラスを使用すると、以下のようになります。こちらの場合は、strtotime() のような制限がありません。PHP 5.2.0 以降を使用している場合は、DateTime クラスを使用した方が良いと思います。

<?php
/**
 * 指定した年月の第[1-5][日-土]曜日の日付を求める
 * DateTime クラス使用版
 */
function calc_day( $year, $month, $week_of_month, $day_of_the_week )
{
	$year_month = sprintf( '%04d-%02d', (int)$year, (int)$month );
	$datetime = new DateTime( $year_month );
	$datetime->modify( (int)$week_of_month . ' ' . $day_of_the_week );

	if ( $year_month !== $datetime->format( 'Y-m' ) ) {
		return FALSE;
	}
	$result = (int)$datetime->format( "j" );
	if ( ! checkdate( $month, $result, $year ) ) {
		return FALSE;
	}
	return $result;
}

使用方法は以下の通りです。

<?php
// 2010年1月の第1日曜日から第5日曜日までを求める
echo calc_day( 2010, 1, 1, 'sun' ) . "\n";
echo calc_day( 2010, 1, 2, 'sun' ) . "\n";
echo calc_day( 2010, 1, 3, 'sun' ) . "\n";
echo calc_day( 2010, 1, 4, 'sun' ) . "\n";
echo calc_day( 2010, 1, 5, 'sun' ) . "\n";

結果は以下のようになります。

3
10
17
24
31

strtotime() や DateTime::modify() で使用可能なキーワード

strtotime() や DateTime::modify() では、英文形式の日付を解釈できることになっていますが、PHP マニュアルには、正式にどのようなキーワードが使用可能なのかが載っていません。PHP 5.3.x のソースには ext/date/lib/parse_date.re に strtotime() や DateTime::modify() で使用可能なキーワードが定義されています。一覧のキーワードと後ろの数値を見るだけで、ある程度、使用方法を想像できるのではないでしょうか。

static timelib_relunit const timelib_relunit_lookup[] = {
	{ "sec",         TIMELIB_SECOND,  1 },
	{ "secs",        TIMELIB_SECOND,  1 },
	{ "second",      TIMELIB_SECOND,  1 },
	{ "seconds",     TIMELIB_SECOND,  1 },
	{ "min",         TIMELIB_MINUTE,  1 },
	{ "mins",        TIMELIB_MINUTE,  1 },
	{ "minute",      TIMELIB_MINUTE,  1 },
	{ "minutes",     TIMELIB_MINUTE,  1 },
	{ "hour",        TIMELIB_HOUR,    1 },
	{ "hours",       TIMELIB_HOUR,    1 },
	{ "day",         TIMELIB_DAY,     1 },
	{ "days",        TIMELIB_DAY,     1 },
	{ "week",        TIMELIB_DAY,     7 },
	{ "weeks",       TIMELIB_DAY,     7 },
	{ "fortnight",   TIMELIB_DAY,    14 },
	{ "fortnights",  TIMELIB_DAY,    14 },
	{ "forthnight",  TIMELIB_DAY,    14 },
	{ "forthnights", TIMELIB_DAY,    14 },
	{ "month",       TIMELIB_MONTH,   1 },
	{ "months",      TIMELIB_MONTH,   1 },
	{ "year",        TIMELIB_YEAR,    1 },
	{ "years",       TIMELIB_YEAR,    1 },

	{ "monday",      TIMELIB_WEEKDAY, 1 },
	{ "mon",         TIMELIB_WEEKDAY, 1 },
	{ "tuesday",     TIMELIB_WEEKDAY, 2 },
	{ "tue",         TIMELIB_WEEKDAY, 2 },
	{ "wednesday",   TIMELIB_WEEKDAY, 3 },
	{ "wed",         TIMELIB_WEEKDAY, 3 },
	{ "thursday",    TIMELIB_WEEKDAY, 4 },
	{ "thu",         TIMELIB_WEEKDAY, 4 },
	{ "friday",      TIMELIB_WEEKDAY, 5 },
	{ "fri",         TIMELIB_WEEKDAY, 5 },
	{ "saturday",    TIMELIB_WEEKDAY, 6 },
	{ "sat",         TIMELIB_WEEKDAY, 6 },
	{ "sunday",      TIMELIB_WEEKDAY, 0 },
	{ "sun",         TIMELIB_WEEKDAY, 0 },

	{ "weekday",     TIMELIB_SPECIAL, TIMELIB_SPECIAL_WEEKDAY },
	{ "weekdays",    TIMELIB_SPECIAL, TIMELIB_SPECIAL_WEEKDAY },
	{ NULL,          0,          0 }
};

/* The relative text table. */
static timelib_lookup_table const timelib_reltext_lookup[] = {
	{ "first",    0,  1 },
	{ "next",     0,  1 },
	{ "second",   0,  2 },
	{ "third",    0,  3 },
	{ "fourth",   0,  4 },
	{ "fifth",    0,  5 },
	{ "sixth",    0,  6 },
	{ "seventh",  0,  7 },
	{ "eight",    0,  8 },
	{ "eighth",   0,  8 },
	{ "ninth",    0,  9 },
	{ "tenth",    0, 10 },
	{ "eleventh", 0, 11 },
	{ "twelfth",  0, 12 },
	{ "last",     0, -1 },
	{ "previous", 0, -1 },
	{ "this",     1,  0 },
	{ NULL,       1,  0 }
};

/* The month table. */
static timelib_lookup_table const timelib_month_lookup[] = {
	{ "jan",  0,  1 },
	{ "feb",  0,  2 },
	{ "mar",  0,  3 },
	{ "apr",  0,  4 },
	{ "may",  0,  5 },
	{ "jun",  0,  6 },
	{ "jul",  0,  7 },
	{ "aug",  0,  8 },
	{ "sep",  0,  9 },
	{ "sept", 0,  9 },
	{ "oct",  0, 10 },
	{ "nov",  0, 11 },
	{ "dec",  0, 12 },
	{ "i",    0,  1 },
	{ "ii",   0,  2 },
	{ "iii",  0,  3 },
	{ "iv",   0,  4 },
	{ "v",    0,  5 },
	{ "vi",   0,  6 },
	{ "vii",  0,  7 },
	{ "viii", 0,  8 },
	{ "ix",   0,  9 },
	{ "x",    0, 10 },
	{ "xi",   0, 11 },
	{ "xii",  0, 12 },

	{ "january",   0,  1 },
	{ "february",  0,  2 },
	{ "march",     0,  3 },
	{ "april",     0,  4 },
	{ "may",       0,  5 },
	{ "june",      0,  6 },
	{ "july",      0,  7 },
	{ "august",    0,  8 },
	{ "september", 0,  9 },
	{ "october",   0, 10 },
	{ "november",  0, 11 },
	{ "december",  0, 12 },
	{  NULL,       0,  0 }
};