Help us understand the problem. What is going on with this article?

「1月31日の一ヶ月後はいつですか」にCarbon や Chronos はどう答えるか

More than 1 year has passed since last update.

日時を扱う処理を書くとき、月をまたぐ部分はうっかりすると不具合の原因になりがち。

たとえば現在の日付が31日であるタイミングでこういうことをやると

\DateTime::createFromFormat('Y-m', '2018-02')

「年」は2018で「月」は2だけど「日」が現在日時の30になるため、月をこえてしまって結果は3月3日になるという悲劇が起きます。

さて今回は日時を生成するときじゃなくて「一ヶ月後」などのお話。
このへん、PHP の日時ライブラリ Carbon と Chronos はどう扱っているのか確認してみます。

Carbon の場合

動作

1月31日の一ヶ月後を求めようとして

\Carbon\Carbon::create(2018, 1, 31)->addMonths(1)

とすると、

2018-03-03

になります。

ただし先にこれをやっておくと

Carbon::useMonthsOverflow(false);

結果はこうなる。

2018-02-28

実装

Carbon の addMonths() ではこういう判定をしてます。

public function addMonths($value)
{
    if (static::shouldOverflowMonths()) {
        return $this->addMonthsWithOverflow($value);
    }

    return $this->addMonthsNoOverflow($value);
}

static::shouldOverflowMonths() が見ているのはこれで

public static function shouldOverflowMonths()
{
    return static::$monthsOverflow;
}

さきほどの Carbon::useMonthsOverflow(false); によって $monthsOverflow がセットされます。

public static function useMonthsOverflow($monthsOverflow = true)
{
    static::$monthsOverflow = $monthsOverflow;
}

実際に月を足し引きしてるのはこのふたつ。

public function addMonthsWithOverflow($value)
{
    return $this->modify((int) $value.' month');
}

public function addMonthsNoOverflow($value)
{
    $day = $this->day;

    $this->modify((int) $value.' month');

    if ($day !== $this->day) {
        $this->modify('last day of previous month');
    }

    return $this;
}

$monthsOverflow のデフォルト値は true になってるからそのままだと月をこえて計算されるけど、これを false にすることによって

一ヶ月後を計算して day が変わるようならそれは月を越えたサインだから、前月の末日に戻す

という処理になります。

Chronos の場合

動作

\Cake\Chronos\Chronos::create(2018, 1, 31)->addMonths(1)

は最初から

2018-02-28

になります。

実装

これは Chronos の実装がこうなっているためで

public function addMonths($value)
{
    $day = $this->day;
    $date = $this->modify((int)$value . ' month');

    if ($date->day !== $day) {
        return $date->modify('last day of previous month');
    }

    return $date;
}

もう月を越えたら無条件に前月の末日に巻き戻してる。

Carbon のデフォルト処理のように月を越えてもそのまま使いたい場合は、明示的に

\Cake\Chronos\Chronos::create(2018, 1, 31)->addMonthsWithOverflow(1)

とすることになるようです。

「一ヶ月後」という処理をするときには Chronos のデフォルト動作の方が求められていることが多いような気もするけど、「Chronos は Carbon の immutable 版」とだけ覚えてしまうとこのへんの違いで思わぬ問題が起きるかも。テストだいじ。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした