We often use the Go time package AddDate() to perform calculations on dates. And the results it gets may often exceed our "expectations". (Why expectations are in quotes, because our expectations can be vague and biased).

Citation

Suppose, today is October 31st, the last day of October, we want to calculate the last day of next month by AddDate() .

 today := time.Date(2022, 10, 31, 0, 0, 0, 0, time.Local)
nextDay := today.AddDate(0, 1, 0)
fmt.Println(nextDay.Format("20060102"))

// 输出:20221201

The resulting output: 20221201 , instead of the last day of the next month, November 30, as we expected.

This is how it is handled in the Go Time package:

  1. AddDate() +1 to the month, that is, 11-31, which is converted into the corresponding number of days, and finally converted into the corresponding number of nanoseconds and stored in the Time object;
  2. When outputting, Format() will output the standard date, and the nanoseconds in Time will be converted to 12-01 instead of 11-31, because this day does not exist;

This problem occurs as long as it involves the last day of the big and small months.

 today := time.Date(2022, 3, 31, 0, 0, 0, 0, time.Local)
d := today.AddDate(0, -1, 0)
fmt.Println(d.Format("20060102"))
// 20220303

today := time.Date(2022, 3, 31, 0, 0, 0, 0, time.Local)
d := today.AddDate(0, 1, 0)
fmt.Println(d.Format("20060102"))
// 20220501

today := time.Date(2022, 10, 31, 0, 0, 0, 0, time.Local)
d := today.AddDate(0, -1, 0)
fmt.Println(d.Format("20060102"))
// 20221001

today := time.Date(2022, 10, 31, 0, 0, 0, 0, time.Local)
d := today.AddDate(0, 1, 0)
fmt.Println(d.Format("20060102"))
// 20221201

Source code analysis

Take a look at the specific source code of the Go Time package, and still start with the example of 10-31 + 1 month as a use case.
AddDate() , first process month+1 and then call Date() .

 // time/time.go

func (t Time) AddDate(years int, months int, days int) Time {
    year, month, day := t.Date() // 获取当前年月日
    hour, min, sec := t.Clock() // 获取当前时分秒
    return Date(year+years, month+Month(months), day+days, hour, min, sec, int(t.nsec()), t.Location())
}

Date() The parameters passed in at this time are

  • year 2020
  • month 11
  • day 31
  • hour, min, sec, nsec are the hours, minutes, seconds, and nanoseconds of the runtime

d calculates the number of days before the absolute epoch to today:
**d = 今年之前的天数 + 年初到当月之前的天数 + 月初到当天之前的天数;**
Finally, store d 转换成纳秒 + 当天经过的纳秒 in the Time object.

 // time/time.go

func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time {
    ……

    // Compute days since the absolute epoch.
    d := daysSinceEpoch(year)

    // Add in days before this month.
    d += uint64(daysBefore[month-1])
    if isLeap(year) && month >= March {
        d++ // February 29
    }

    // Add in days before today.
    d += uint64(day - 1)

    // Add in time elapsed today.
    abs := d * secondsPerDay
    abs += uint64(hour*secondsPerHour + min*secondsPerMinute + sec)

    ……
    return t
}

Entering 2022-11-31 and entering 2022-12-01 for Date() will get the same d (number of days). Both are the same data when they are stored in the bottom layer. When Format(), the Time of ---d8140f9233e30b2146457f41042b3de9 2022-11-31 is formatted as 2022-12-01 is no exception. Of course, the output should be displayed so that people can understand it. The regular standard date.

 // 2022-11-31
d = 2022年之前的天数 + 1月到10月的总天数 + 30天

// 2022-12-01
d = 2022年之前的天数 + 1月到11月的总天数 + 0天
  = 2022年之前的天数 + 1月到10月的总天数 + 30天 + 0天

You can even enter a non-standard date 2022-11-35 into Date(), which will get the same d (days) as a standard date 2022-12-05 .
"Non-standard date" and "standard date" are like two sides of the scale, although the form is different, but their actual mass (d days) is the same. Remember this sentence, it will be useful later.

expected deviation

We figured out the principle, but still can't accept the result. Is this result a Go bug? Or is the Go Time package lazy?
However, it is not, it is precisely our "expectation" that has a problem.
Normally, we would expect 10-30 + 1 month to be 11-30 day, which is reasonable. Then why do we still expect 10-31 + 1 month also 11-30 day? Just because 10-31 is the last day of the current month, do we also expect +1 month to be the last day of the next month?
The two dates of 10-30 and 10-31 differ by one day, and after the same +1 month operation, it becomes the same day. This is the same result as 1 + 10 = 2 + 10, which is obviously unreasonable.
Go's current processing results are correct, and he also noted in the AddDate() comments that it will handle "overflow". Moreover, not only the Go language, but also PHP, see the confusing strtotime in Bird's article - the corner of the wind and snow .

How to deal with it

I understand the reason, but what if I just want to get the last day of the previous/next month?
Using the "balance principle" mentioned in the previous source code analysis stage, we can get the results we want.

 today := time.Date(2022, 10, 31, 0, 0, 0, 0, time.Local)
d := today.Day()

// 上个月最后一天
// 10-00 日 等于 9-30 日
day1 := today.AddDate(0, 0, -d)
fmt.Println(day1.Format("20060102"))

// 下个月最后一天
// 12-00 日 等于 11-30 日
day2 := today.AddDate(0, 2, -d)
fmt.Println(day2.Format("20060102"))

// 20220930
// 20221130

Epilogue

At first, I found this problem by reading Brother Bird's article. At that time, I thought it was a "pit" of PHP, and I didn't think about it in depth. Today, I encounter this problem again in the Go language, rethink it, and find that the date function should be designed that way. It is because we don't understand the date function enough and have wrong "expectations".

Article from confusing Go time.AddDate

Mr_houzi
964 声望22 粉丝