LoginSignup
79

More than 5 years have passed since last update.

日付時刻関連のクラスを活用しよう

Last updated at Posted at 2015-03-06

以下も併せてお読みください。

クラス一覧

クラス名 (実装しているインターフェースも示す) イミュータブル 用途
DateTimeZone タイムゾーン
DateInterval 時間
DateTimeInterface DateTime × 日付時刻
DateTimeImmutable
Traversable DatePeriod 期間

各クラスの基本的な使い方

DateTimeZoneクラス

タイムゾーンを定義するクラスです。このクラスは再度外部から能動的に__construct()をコールしない限りはイミュータブルが保証されます。

インスタンスの生成

サポートされるタイムゾーンのリストにある名称または+0900のような時差を表す文字列を第1引数としてコンストラクタに与えます。

現在 (2通り)
$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してみると実際に設定されているプロパティが確認できます。詳細に関しては時間の入力書式を参照してください。

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引数としてコンストラクタに与えます。

現在 (3通り)
$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番目の例だけは意味が異なることに注意してください。これは以下のような解釈が為されるものです。

  1. date_default_timezone_getで得られるデフォルトタイムゾーンに従った現在時刻を得る。
  2. その時刻がタイムゾーン+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
setDateメソッドを利用
$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
setISODateメソッドを利用
$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
setTimeメソッドを利用
$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
setTimezoneメソッドを利用
$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
Datetime::modifyメソッドで代用 (2通り)
$date->modify('2015-03-18');
$date->modify('2015-W12-3');
__constructを能動的にコール
$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
\$dateに2日間足して、それから\$a(7日間)の分だけ足し、更に$b(1日間)の分だけ引く
$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オブジェクトを利用します。

2015-01-01とその1年後の差を求める
$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によって生成されたときにのみ自動で設定されるものであり、総日数を表します。

DateInteval::formatで総日数を表示させることもできる
$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を異常だと考えるべきです。

\$dateに2日間足して、それから\$a(7日間)の分だけ足し、更に\$b(1日間)の分だけ引いたものを\$newdateに代入する
$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以降であることにご注意ください。

screenshot.2015-03-06.png

インスタンスの生成 (「開始日時」「反復間隔」「終了日時」の組み合わせ)

2015-01-01から1日間隔で2015-01-05の直前まで繰り返す
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
)
*/

インスタンスの生成 (「開始日時」「反復間隔]」「反復回数」の組み合わせ)

2015-01-01から1日間隔で3回追加反復する
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書式」)

期間の入力書式に従います。

上で紹介したものは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を追加の引数として渡します。

2015-01-01の直後から1日間隔で2015-01-05の直前まで繰り返す
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分だけ終了日時を引き延ばしておきます。

2015-01-01から1日間隔で2015-01-06の直前まで繰り返す
$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')
    );
}

バグ情報

日付時刻関連のバグは非常に多いと思われます。公式に報告されているバグやご自身のブログ等に投稿されているバグなどで、注意すべきものがあれば報告していただければこちらに追加で掲載致します。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
79