Description
Proposal Details
Summary
Go’s existing time.Time.AddDate
method preserves RFC 3339-style normalization (rolling over invalid dates to the next valid date), which can produce unintuitive results when adding or subtracting months on end-of-month dates (e.g., time.Date(2025, 3, 31, 0,0,0,0, time.UTC).AddDate(0, -1, 0)
yields March 2nd, not February 28th).
#31145
#66632
By contrast, legal definitions—such as the UK’s Interpretation Act 1978 and analogous Australian statutes—define a calendar month addition to yield the last day of the shorter month when no corresponding day exists (i.e., Feb 28th in non-leap years).
Legislation.gov.uk
AustLII
legalclarity.org
To align Go’s standard library with this intuitive, legally grounded behavior, and to prevent latent off-by-days bugs, I would like to propose adding a new method to the time package:
func (t Time) AddCalendarMonths(months int) Time
Background and Motivation
Unintuitive Behavior of AddDate
-
The current
time.Time.AddDate
method “normalizes its result in the same way that Date does,” meaning adding months moves by the number of days difference, not by “calendar months”
Google Groups. -
In practice, subtracting one month from March 31, 2025 yields March 2, 2025—because February has only 28 days, so Go subtracts 31 − 28 = 3 days from March 31, landing on March 2
time AddDate(0, -1, 0) does not work for March. #31145. -
Such corner-case behavior can silently introduce bugs that go unnoticed until end-of-month dates occur, especially in financial, scheduling, and legal applications.
-
An example that caused this to come to my attention was when trying to get the integer value of the month one calendar month before time.Now(), I got 3 as a result when the day was march 31st (expecting 2 as a result, meaning february). This then offset some further calculations that relied on this month value.
Legal and Conventional Definitions
-
UK’s Interpretation Act 1978 defines a “month” to mean a calendar month, implying that adding one month to January 31 should yield February 28 (or 29), the last day of February
Legislation.gov.uk. -
Australian legislation (e.g., Northern Territory’s Interpretation Act 1978) explicitly defines a “calendar month” as ending on the last day of the next month when no corresponding day exists, matching the intuitive legal standard
AustLII. -
"Months with varying lengths require careful consideration. For example, a “calendar month” starting January 31 would end on February 28 (or February 29 in a leap year). This prevents disputes over deadlines and aligns with the common understanding of the term."
legalclarity.org.
Aligning Go’s API with these well-understood conventions will make date arithmetic more intuitive and reduce defects.
Proposal
package time
// AddCalendarMonths returns t shifted by m calendar months.
// If the target month has fewer days than the starting day,
// AddCalendarMonths returns the last day of the target month.
// The time of day and monotonic clock reading are preserved.
func (t Time) AddCalendarMonths(months int) Time {
t2 := t.AddDate(0, months, 0)
if t2.Day() == t.Day() {
return t2
}
return t2.AddDate(0, 0, -t2.Day())
}
Examples
t1 := time.Date(2025, 3, 31, 12, 0, 0, 0, time.UTC)
fmt.Println(t1.AddCalendarMonths(-1))
// Output: 2025-02-28 12:00:00 +0000 UTC
t2 := time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC)
fmt.Println(t2.AddCalendarMonths(1))
// Output: 2024-02-29 00:00:00 +0000 UTC (leap year)
Compatibility and Scope
-
This addition is backwards-compatible: existing
AddDate
method remains unchanged. -
It applies to API changes in the standard library, which are in scope for the proposal process.
-
It does not affect Go 1 compatibility guarantees, as it merely adds a method.