Skip to content

proposal: import/path: proposal title #73511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mansguiche opened this issue Apr 27, 2025 · 2 comments
Closed

proposal: import/path: proposal title #73511

mansguiche opened this issue Apr 27, 2025 · 2 comments
Labels
LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool Proposal
Milestone

Comments

@mansguiche
Copy link

mansguiche commented Apr 27, 2025

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.

@gopherbot gopherbot added this to the Proposal milestone Apr 27, 2025
@gabyhelp
Copy link

@gabyhelp gabyhelp added the LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool label Apr 27, 2025
@seankhliao
Copy link
Member

I believe use in legal situations will require a more general concept of civil time detached from absolute time, which is what time.Time is (e.g. do you adjust for day?).

see #19700

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool Proposal
Projects
None yet
Development

No branches or pull requests

4 participants