以下も併せてお読みください。
クラス一覧
クラス名 (実装しているインターフェースも示す) | イミュータブル | 用途 | |
---|---|---|---|
DateTimeZone | ○ | タイムゾーン | |
DateInterval | ○ | 時間 | |
DateTimeInterface |
DateTime | × | 日付時刻 |
DateTimeImmutable | ○ | ||
Traversable |
DatePeriod | ○ | 期間 |
各クラスの基本的な使い方
DateTimeZoneクラス
タイムゾーンを定義するクラスです。このクラスは再度外部から能動的に__construct()
をコールしない限りはイミュータブルが保証されます。
インスタンスの生成
サポートされるタイムゾーンのリストにある名称または+0900
のような時差を表す文字列を第1引数としてコンストラクタに与えます。
$tz = new \DateTimeZone('Asia/Tokyo');
$tz = new \DateTimeZone('+0900');
DateIntervalクラス
時間を定義するクラスです。このクラスは再度外部から能動的に__construct()
をコールしない限りはイミュータブルが保証されます。
インスタンスの生成 (時間の入力書式を利用)
時間の入力書式に従う文字列を第1引数としてコンストラクタに与えます。
$two_days = new \DateInterval('P2D');
$two_weeks = new \DateInterval('P2W');
$two_seconds = new \DateInterval('PT2S');
$six_years_and_five_minutes = new \DateInterval('P6YT5M');
インスタンスの生成 (日付時刻の入力書式を利用)
日付時刻の入力書式に従う文字列を第1引数としてDateInterval::createFromDateStringに与えます。
$two_days = \DateInterval::createFromDateString('2 days');
$two_weeks = \DateInterval::createFromDateString('2 weeks');
$two_seconds = \DateInterval::createFromDateString('2 seconds');
$six_years_and_five_minutes = \DateInterval::createFromDateString('6 years 5 minutes');
こちらの方法を用いると、より複雑な表現も可能です。
// 平日1日ごと
$next_weekdays = \DateInterval::createFromDateString('next weekdays');
// 月末ごと
$last_day_of_next_month = \DateInterval::createFromDateString('last day of next month');
フォーマット出力
時間の出力書式に従う文字列を第1引数としてDateInterval::formatに与えます。
$interval = new \DateInterval('P2DT6H');
echo $interval->format('%d日と%h時間'); // 2日と6時間
var_dumpしてみると実際に設定されているプロパティが確認できます。詳細に関しては時間の入力書式を参照してください。
$interval = new \DateInterval('P2DT6H');
var_dump($interval);
/*
object(DateInterval)#1 (15) {
["y"]=>
int(0)
["m"]=>
int(0)
["d"]=>
int(2)
["h"]=>
int(6)
["i"]=>
int(0)
["s"]=>
int(0)
["weekday"]=>
int(0)
["weekday_behavior"]=>
int(0)
["first_last_day_of"]=>
int(0)
["invert"]=>
int(0)
["days"]=>
bool(false)
["special_type"]=>
int(0)
["special_amount"]=>
int(0)
["have_weekday_relative"]=>
int(0)
["have_special_relative"]=>
int(0)
}
*/
DateTimeクラス
日付時刻を定義するクラスです。このクラスはミュータブルに設計されているので、再度外部から能動的に__construct()
をコールする、なんて変なことしなくても内部状態が変化します。
インスタンスの生成
日付時刻の入力書式に従う文字列を第1引数としてコンストラクタに与えます。
$date = new \DateTime;
$date = new \DateTime();
$date = new \DateTime('now');
インスタンスの生成 (タイムゾーンを指定)
日付時刻の入力書式に従う文字列を第1引数、DateTimeZoneを第2引数としてコンストラクタに与えます。書式中で指定することも可能ではありますが、あまり推奨されません。また、両方を指定した場合は書式中のタイムゾーンが優先されます。
$date = new \DateTime('now', new \DateTimeZone('Asia/Tokyo')); // OK
$date = new \DateTime('now', new \DateTimeZone('+0900')); // OK
$date = new \DateTime('now Asia/Tokyo'); // 非推奨
$date = new \DateTime('now +0900'); // そもそも意味が異なる!ダメ
上記の例の4番目の例だけは意味が異なることに注意してください。これは以下のような解釈が為されるものです。
- date_default_timezone_getで得られるデフォルトタイムゾーンに従った現在時刻を得る。
- その時刻がタイムゾーン
+0900
のものであると見なす。
バグか仕様のどちらであるかは不明ですが、何れにせよ進んで使うべき書式ではないとは言えるでしょう。相対指定を用いる際は特に書式中で指定せずにDateTimeZoneオブジェクトとして第2引数で与えることが強く推奨されます。
インスタンスの生成 (書式を指定)
日付時刻の入力書式定義のための書式に従う文字列を第1引数、自分で定義した書式に従う文字列を第2引数としてDateTime::createFromFormatに与えます。コンストラクタをそのまま使うだけでは解釈に曖昧さが出る問題や、日本語に対応していないといった問題を解消することが出来る有用なメソッドです。外部入力に対しての利用が特に推奨されます。
$date = \DateTime::createFromFormat('Y年n月j?*G時i分s秒', '2015年3月18日 13時04分06秒');
if ($date !== false) {
echo $date->format('Y-m-d H:i:s'); // 2015-03-18 13:04:06
}
フォーマット出力
日付時刻の出力書式に従う文字列を第1引数としてDateTimeInterface::formatに与えます。
$date = new \DateTime('2015-01-02 15:04:05');
$ampm = ['am' => '午前', 'pm' => '午後'];
echo $date->format('Y年m月d日') . $ampm[$date->format('a')] . $date->format('h時i分s秒');
// 2015年01月02日午後03時04分05秒
$date = new \DateTime('2015-01-02 15:04:05');
$ampm = ['am' => '午前', 'pm' => '午後'];
echo $date->format('Y年n月j日') . $ampm[$date->format('a')] . $date->format('g時')
. ltrim($date->format('i分'), 0) . ltrim($date->format('s秒'), 0);
// 2015年1月2日午後3時4分5秒
内部状態の変更
既に生成したオブジェクトが持つ日付時刻情報を後から操作するためのメソッドが用意されています。ここでは__construct()
も後から操作するためのメソッドの一つとして見なします。
| メソッド名 | 第1引数 | 第2引数 | 第3引数 | 返り値 | 範囲外の月日時分秒の指定 | 注記 |
|:----:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| DateTime::__construct | 日付時刻
(文字列) | DateTimeZone | | NULL
|
-
31
日以内は自動補正 - それ以外はExceptionをスロー
| DateTime::modify | 日付時刻
(文字列) | | |
$this
| -
31
日以内は自動補正 - それ以外はExceptionをスロー
- タイムゾーン不可
- タイムスタンプ不可
| DateTime::setDate | 年
(整数) | 月
(整数) | 日
(整数) |
$this
| 自動補正 | 時刻には影響無し|| DateTime::setISODate | 年
(整数) | 週番号
(整数) | 曜日番号
(整数) |
$this
| 自動補正 | 時刻には影響無し || DateTime::setTime | 時
(整数) | 分
(整数) | 秒
(整数) |
$this
| 自動補正 | 日付には影響無し || DateTime::setTimestamp | タイムスタンプ
(整数) | | |
$this
| | 32bit環境では2038年問題に注意 || DateTime::setTimezone | DateTimeZone | | |
$this
| | 日付時刻の再計算が行われる || DateTime::add | 加算するDateInterval | | |
$this
| | || DateTime::sub | 減算するDateInterval | | |
$this
| | |
$date = new \DateTime('13:04:06', new \DateTimeZone('Asia/Tokyo'));
$date->setDate(2015, 2, 46); // 2月46日は存在しないので自動補正がかかる
echo $date->format('Y-m-d H:i:s e'); // 2015-03-18 13:04:06 Asia/Tokyo
$date = new \DateTime('13:04:06', new \DateTimeZone('Asia/Tokyo'));
$date->setISODate(2015, 12, 3); // 12週目の水曜日
echo $date->format('Y-m-d H:i:s e'); // 2015-03-18 13:04:06 Asia/Tokyo
$date = new \DateTime('2015-03-18', new \DateTimeZone('Asia/Tokyo'));
$date->setTime(13, 4, 6);
echo $date->format('Y-m-d H:i:s e'); // 2015-03-18 13:04:06 Asia/Tokyo
$date = new \DateTime('2015-03-10 04:04:06', new \DateTimeZone('UTC'));
$date->setTimezone(new \DateTimeZone('Asia/Tokyo'));
echo $date->format('Y-m-d H:i:s e'); // 2015-03-10 13:04:06 Asia/Tokyo
$date->modify('2015-03-18');
$date->modify('2015-W12-3');
$date = new \DateTime('now', new \DateTimeZone('UTC'));
$date->__construct('2015-03-18 13:04:06', new \DateTimeZone('Asia/Tokyo'));
echo $date->format('Y-m-d H:i:s e'); // 2015-03-18 13:04:06 Asia/Tokyo
$a = new \DateInterval('P7D');
$b = new \DateInterval('P1D');
$date = new \DateTime('2015-03-10');
$date->modify('+2 days')->add($a)->sub($b);
echo $date->format('Y-m-d'); // 2015-03-18
2つのDateTimeInterfaceの差を求める
DateTimeInterface::diffを実行し、返り値として得られるDateIntervalオブジェクトを利用します。
$today = new \DateTime('2015-01-01');
$next_year = new \DateTime('2016-01-01');
var_dump($today->diff($next_year));
/*
object(DateInterval)#3 (15) {
["y"]=>
int(1)
["m"]=>
int(0)
["d"]=>
int(0)
["h"]=>
int(0)
["i"]=>
int(0)
["s"]=>
int(0)
["weekday"]=>
int(0)
["weekday_behavior"]=>
int(0)
["first_last_day_of"]=>
int(0)
["invert"]=>
int(0)
["days"]=>
int(366)
["special_type"]=>
int(0)
["special_amount"]=>
int(0)
["have_weekday_relative"]=>
int(0)
["have_special_relative"]=>
int(0)
}
*/
当たり前ですが、var_dumpの結果、1年の差があることが確認できました。更に、最初にDateIntervalを自分で生成したときとは異なり、**days
**というプロパティが設定されていることも確認できます。このプロパティはDateTimeInterface::diffによって生成されたときにのみ自動で設定されるものであり、総日数を表します。
$today = new \DateTime('2015-01-01');
$next_year = new \DateTime('2016-01-01');
echo $today->diff($next_year)->format('%a日間'); // 366日間 (2016年は閏年)
また、この演算によって得られた結果が正の時間になっていることも意識してください。時間の入力書式でも説明していますが、invert
というプロパティは正負の逆転を意味するものであり、DateTimeInterface::diffで小さいほうから大きいほうを引く演算が行われたときにのみ、自動で1
に設定されるようになっています。今回は 0
のままなので正を意味します。
…ところが、少し違和感がありませんか?
$today->diff($next_year)
このコード、直感的には「呼び出し元から引数を引く」ように感じる人が多いのではないでしょうか?実際はその逆です。「引数から呼び出し元を引く」と考えるのが正しいです。誤解を招きやすい設計になっているので十分留意してください。この点はDateTime::subとは大きく感覚が異なります。
DateTimeImmutableクラス
DateTimeクラスとほとんど同じメソッドを持ちます。このクラスはDateTimeクラスとは異なり、再度外部から能動的に__construct()
をコールしない限りはイミュータブルが保証されます。PHP5.4以前との互換性を気にする必要がない場合、積極的にDateTimeの代わりにDateTimeImmutableを使うべきであると言えるでしょう。本来DateTimeがイミュータブルに設計されるべきであるにも関わらず、ミュータブルな設計になっていたPHPを異常だと考えるべきです。
$a = new \DateInterval('P7D');
$b = new \DateInterval('P1D');
$date = new \DateTimeImmutable('2015-03-10');
$newdate = $date->modify('+2 days')->add($a)->sub($b);
echo $date->format('Y-m-d') . PHP_EOL; // 2015-03-10 (変更されていない)
echo $newdate->format('Y-m-d') . PHP_EOL; // 2015-03-18
modify
は英単語的には「修正する」という意味ですが、新しくインスタンスを作り直しているあたりがやや直感とのギャップを生んでいるかもしれません。
DatePeriodクラス
一定区間または一定回数の間だけ、DateIntervalごとにDateTimeInterfaceを取り出すクラスです。Iterator
インターフェースは実装されていませんが、Traversable
インターフェースはされているので、foreach
構文で取り扱ったりiterator_to_array関数で配列に変換したりすることができます。
コンストラクタは引数の取り方が3通りあります。
public DatePeriod::__construct ( DateTimeInterface $start , DateInterval $interval , int $recurrences [, int $options ] )
public DatePeriod::__construct ( DateTimeInterface $start , DateInterval $interval , DateTimeInterface $end [, int $options ] )
public DatePeriod::__construct ( string $isostr [, int $options ] )
但し、DateTimeImmutableを完全に受け付けられるようになったのは5.5.8
以降であることにご注意ください。
インスタンスの生成 (「開始日時」「反復間隔」「終了日時」の組み合わせ)
print_r(array_map(
function (\DateTimeInterface $date) { return $date->format('Y-m-d'); },
iterator_to_array(new \DatePeriod(
new \DateTime('2015-01-01'), // 開始日時 (ここから)
new \DateInterval('P1D'), // 反復間隔 (この間隔で)
new \DateTime('2015-01-05') // 終了日時 (ここの "直前" まで繰り返す)
))
));
/*
Array
(
[0] => 2015-01-01
[1] => 2015-01-02
[2] => 2015-01-03
[3] => 2015-01-04
)
*/
インスタンスの生成 (「開始日時」「反復間隔]」「反復回数」の組み合わせ)
print_r(array_map(
function (\DateTimeInterface $date) { return $date->format('Y-m-d'); },
iterator_to_array(new \DatePeriod(
new \DateTime('2015-01-01'), // 開始日時 (ここから)
new \DateInterval('P1D'), // 反復間隔 (この間隔で)
3 // 反復回数 (この回数だけ追加で繰り返す)
))
));
/*
Array
(
[0] => 2015-01-01
[1] => 2015-01-02
[2] => 2015-01-03
[3] => 2015-01-04
)
*/
インスタンスの生成 (「ISO8601書式」)
期間の入力書式に従います。
print_r(array_map(
function (\DateTimeInterface $date) { return $date->format('Y-m-d'); },
iterator_to_array(new \DatePeriod('2015-01-01T00:00:00Z/P1D/2015-01-05T00:00:00Z'))
));
print_r(array_map(
function (\DateTimeInterface $date) { return $date->format('Y-m-d'); },
iterator_to_array(new \DatePeriod('2015-01-01T00:00:00Z/P1D/R3'))
));
インスタンスの生成 (開始日の除外)
DatePeriod::EXCLUDE_START_DATE
を追加の引数として渡します。
print_r(array_map(
function (\DateTimeInterface $date) { return $date->format('Y-m-d'); },
iterator_to_array(new \DatePeriod(
new \DateTime('2015-01-01'), // 開始日時 (ここから)
new \DateInterval('P1D'), // 反復間隔 (この間隔で)
new \DateTime('2015-01-05'), // 終了日時 (ここの "直前" まで繰り返す)
\DatePeriod::EXCLUDE_START_DATE // 但し開始日を除外する
))
));
/*
Array
(
[0] => 2015-01-02
[1] => 2015-01-03
[2] => 2015-01-04
)
*/
インスタンスの生成 (終了日の包含)
指定するDateInterval分だけ終了日時を引き延ばしておきます。
$interval = new \DateInterval('P1D');
print_r(array_map(
function (\DateTimeInterface $date) { return $date->format('Y-m-d'); },
iterator_to_array(new \DatePeriod(
new \DateTime('2015-01-01'), // 開始日時 (ここから)
$interval, // 反復間隔 (この間隔で)
(new \DateTime('2015-01-05'))->add($interval) // 終了日時 (ここの "直前" まで繰り返す)
))
));
/*
Array
(
[0] => 2015-01-01
[1] => 2015-01-02
[2] => 2015-01-03
[3] => 2015-01-04
[4] => 2015-01-05
)
*/
応用例
指定したDateTimeImmutableが属する月を1日単位で走査するDatePeriodを返す関数
簡単に言うと「1ヵ月の取得」です。
function get_month_period(\DateTimeImmutable $today) {
return new \DatePeriod(
$today->modify('today first day of'),
new \DateInterval('P1D'),
$today->modify('today first day of next month')
);
}
foreach (get_month_period(new \DateTimeImmutable('2015-02-15')) as $date) {
echo $date->format('Y-m-d') . PHP_EOL;
}
/*
2015-02-01
2015-02-02
2015-02-03
...
2015-02-26
2015-02-27
2015-02-28
*/
指定した2つのDateTimeImmutableが属する月同士の区間中に存在する全ての月末日を走査するDatePeriodを返す関数
簡単に言うと「全ての月末日の取得」です。
function get_last_days_period(\DateTimeImmutable $start, \DateTimeImmutable $end) {
return new \DatePeriod(
$start->modify('today last day of'),
\DateInterval::createFromDateString('last day of next month'),
$end->modify('today first day of next month')
);
}
foreach (get_last_days_period(
new \DateTimeImmutable('2015-09-27'),
new \DateTimeImmutable('2016-03-15')
) as $date) {
echo $date->format('Y-m-d') . PHP_EOL;
}
/*
2015-09-30
2015-10-31
2015-11-30
2015-12-31
2016-01-31
2016-02-29
2016-03-31
*/
指定したDateTimeImmutableが属する月が属する全ての日曜開始週を1日単位で走査するDatePeriodを返す関数
簡単に言うと「カレンダー」です。@rana_kualuさんが**「PHPでナウいカレンダー」**として実装されているものをもう少しシンプルにしてみました。日曜週開始の扱いに不安があったので、一応テストも行ってみました。
function get_calendar_period(\DateTimeImmutable $today) {
return new \DatePeriod(
$today->modify('first day of')->modify('Sunday last week'),
new \DateInterval('P1D'),
$today->modify('last day of')->modify('Sunday this week')
);
}
foreach (get_calendar_period(new \DateTimeImmutable('2016-02-28')) as $i => $date) {
echo $date->format('d ');
if ($i % 7 === 6) {
echo PHP_EOL;
}
}
/*
31 01 02 03 04 05 06
07 08 09 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 01 02 03 04 05
*/
// オブジェクトのままでは比較が面倒なので文字列に変換
function to_date_string(\DateTimeInterface $date) {
return $date->format('Y-m-d');
}
// 年月を指定してテスト
function test($year, $month) {
assert("
array_map('to_date_string', iterator_to_array((new Calendar($year, $month))->getCalendar()))
===
array_map('to_date_string', iterator_to_array(get_calendar_period(new \DateTimeImmutable('$year-$month'))))
");
}
// 1970年1月~2099年12月までをチェックしてみる (何も出力されなければ成功)
foreach (range(1970, 2099) as $year) {
foreach (range(1, 12) as $month) {
test($year, $month);
}
}
なお、もし月曜開始にしたい場合は以下のように変更してください。
function get_calendar_period(\DateTimeImmutable $today) {
return new \DatePeriod(
$today->modify('first day of')->modify('Monday this week'),
new \DateInterval('P1D'),
$today->modify('last day of')->modify('Monday next week')
);
}
バグ情報
日付時刻関連のバグは非常に多いと思われます。公式に報告されているバグやご自身のブログ等に投稿されているバグなどで、注意すべきものがあれば報告していただければこちらに追加で掲載致します。