DEV Community

meijin
meijin

Posted on

[PHP/Carbon]The result varies by subMonth, subRealMonth, subMonthNoOverflow

An error occurred when the end of March

In the end of March, suddenly my CI threw errors running PHPUnit.
The reason is that difference from subMonth, subRealMonth, subMonthNoOverflow in Carbon, which is PHP-date library.

Version of Carbon

  • 2.43.0

Results at the end of March

I executed following PHPUnit code, and checked the result.

    /**
     * When subtract one month from the date at the end of March, you get 28 days before, 30 days before, and exactly one month before, respectively.
     */
    public function test_3月末の日付から1ヶ月引くとそれぞれ28日前30日前正確な1月前になる()
    {
        Carbon::setTestNow(Carbon::create(2001, 3, 31, 10));
        self::assertEquals(
            Carbon::create(2001, 3, 3, 10),
            Carbon::now()->subMonth(),
        );
        self::assertEquals(
            Carbon::now()->subDays(30),
            Carbon::now()->subRealMonth(),
        );
        self::assertEquals(
            Carbon::create(2001, 2, 28, 10),
            Carbon::now()->subMonthNoOverflow(),
        );
    }
Enter fullscreen mode Exit fullscreen mode

The result of subMonth is March 3, which is 3 days after end of February. Why?
Because subMonth judged the date is February 31. But it does not exist. And February 31 is equally March 3. So the result is March 3.

The result of subRealMonth is just 30 days ago.

Finally, subMonthNoOverflow outputted the end of February!
Because the method rounds down overflowed date, so February 31 becomes February 28.

In short, subMonth and subRealMonth sometimes output day in current Month.

Results at the end of March(Leap year)

And, in leap years, the methods has same logic.

    /**
     * When subtract one month from the end of March in a leap year, you get 29 days before, 30 days before, and exactly one month before, respectively.
     */
    public function test_うるう年の3月末から1ヶ月引くとそれぞれ29日前30日前正確な1月前になる()
    {
        Carbon::setTestNow(Carbon::create(2000, 3, 31, 10));
        self::assertEquals(
            Carbon::create(2001, 3, 2, 10),
            Carbon::now()->subMonth(),
        );
        self::assertEquals(
            Carbon::now()->subDays(30),
            Carbon::now()->subRealMonth(),
        );
        self::assertEquals(
            Carbon::create(2000, 2, 29, 10),
            Carbon::now()->subMonthNoOverflow(),
        );
    }
Enter fullscreen mode Exit fullscreen mode

The difference point is subMonth's result.
In leap years, February 31 equals March 2 because February has 29 days.


Therefore, I recommend subMonthNoOverflow method when you should subtract months from specific date. The result is intuitive.

How to calculate the 15th of the previous month

According to difference of these methods, let's check how to calculate the 15th of the previous month.

    /**
     * When the requirement is to calculate 0:00:00 on the 15th of the previous month, care should be taken to execute startOfMonth() first, or subMonthNoOverflow() should be used.
     */
    public function test_前月の15日0時0分を算出するという要件の時は先にstartOfMonthを実行するように気をつけるかsubMonthNoOverflowを使うべき()
    {
        Carbon::setTestNow(Carbon::create(2001, 3, 31));
        self::assertEquals(
            Carbon::create(2001, 2, 15),
            Carbon::now()->startOfMonth()->subMonth()->addDays(14),
        );

        // when subMonth executed before startOfMonth, the month is March...
        self::assertEquals(
            Carbon::create(2001, 3, 15),
            Carbon::now()->subMonth()->startOfMonth()->addDays(14),
        );

        self::assertEquals(
            Carbon::create(2001, 2, 15),
            Carbon::now()->subMonthNoOverflow()->startOfMonth()->addDays(14),
        );

        self::assertEquals(
            Carbon::create(2001, 2, 15),
            Carbon::now()->startOfMonth()->subMonthNoOverflow()->addDays(14),
        );
    }
Enter fullscreen mode Exit fullscreen mode

Conclusion

Use subMonthNoOverflow instead of subMonth, subRealMonth. The result is always intuitive. In limited case, subMonth and subRealMonth should be used.

Top comments (1)

Collapse
 
rogierw profile image
Rogier

Thanks! I am facing this problem right now, and this explains it.