Skip to content

proposal: import/path: proposal title #73511

Closed as duplicate of#19700
Closed as duplicate of#19700
@mansguiche

Description

@mansguiche

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions