Skip to content
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

time AddDate(0, -1, 0) does not work for March. #31145

Closed
happilymarrieddad opened this issue Mar 29, 2019 · 29 comments
Closed

time AddDate(0, -1, 0) does not work for March. #31145

happilymarrieddad opened this issue Mar 29, 2019 · 29 comments

Comments

@happilymarrieddad
Copy link

What version of Go are you using (go version)?

$ go version
go version go1.12.1 linux/amd64

Does this issue reproduce with the latest release?

Yes, I tested it on golang.org and my local machine

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/nick/.cache/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/nick/Projects/Go"
GOPROXY=""
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build101166364=/tmp/go-build -gno-record-gcc-switches"

What did you do?

I'm trying to go back 1 month from March and I get a really weird result. It goes from March 30th to March 2nd. I'm assuming it's taking the number of days in February and subtracting that from March. It should take the number of days in March and subtract it.

https://play.golang.org/p/n2xvzVFnA4G

What did you expect to see?

It should have been a date in February at least. Momentjs, not I don't like javascript, sets it to February 28 which is a little weird but at least it's February.

What did you see instead?

It set it to March 2nd.

@happilymarrieddad
Copy link
Author

I'm going to try to tackle this one. I think I might be able to fix it. I'm pretty sure I know what the problem is.

@ghost
Copy link

ghost commented Mar 29, 2019

I think the result is complying with the documentation:

AddDate normalizes its result in the same way that Date does, so, for example, adding one month to October 31 yields December 1, the normalized form for November 31.

So in your example: Mar-30-2019 - 01-00-0000 = Feb-30-2019, which, normalized, is Mar-02-2019.

@happilymarrieddad
Copy link
Author

Oh ok, so it's supposed to do that. Is there a way to "unnormalize" it? Yea, I just got all my contrib all set up and was looking at this part of the code when I saw this message.

@happilymarrieddad
Copy link
Author

It would seem to me that the proper solution would be the number of days in a month vs the day set after the month has been decremented and whatever is smaller is the day you pick. I'm not a date expert though. Anyways, thanks!

@ianlancetaylor
Copy link
Member

Closing because this is working as documented and expected.

@vysdel
Copy link

vysdel commented Apr 1, 2019

Closing because this is working as documented and expected.

But how this behavior is expected?
Please check this https://play.golang.org/p/99iEgshLy6T

Sub one month from 2019 March 30 results in March 02
Sub one month from 2019 April 01 results in March 01

This is definitely a bug. It must be Feb 28. duckduckgo.com as well as other various resources show Feb 28

@happilymarrieddad
Copy link
Author

@vysdel Yea, I agree but the masters that be seem to not think so. I was going to write my own date library to handle it the way I think is proper. Probably not the best solution but I need it to handle dates another way. I'll post here I guess when I finish my package.

@ianlancetaylor
Copy link
Member

@vysdel Subtracting one month from March 30 gives you February 30. The date February 30 does not exist, so it is normalized, as documented for time.Time.AddDate and time.Date, to March 2.

Subtracting one month from April 1 gives you March 1. That date does exist, so no normalization is required.

What are you really trying to do?

@happilymarrieddad
Copy link
Author

happilymarrieddad commented Apr 2, 2019

Please note: before you read this message, that Moment.js and Java do not exhibit this behavior.

@iamoryanmoshe I think what he and I am saying is that you say it's documented but there is no explanation. The term normalization means means changing something into a standard. I've never heard of the standard you refer to where you go back one month and you get a date in the same month... Also if you go forward from January you end up in March. How is going from January to March in any sense of the word going forward 1 month... I've done quite a bit of google searching and I can't find any examples of this behavior. It looks like to me someone didn't want to implement the extremely complicated cases. I've been working on writing a package for it and I've already spent a lot of time on it. There are a lot of things to consider like leap year, yes this affects it because of Feb 29, and all the other cases. You ask "What are you really trying to do?" Did you look at the two examples that are provided? I hope this helps explain the situation and why we are frustrated with the responses that we've been given. Thanks so much for taking the time to read this.

@happilymarrieddad
Copy link
Author

Selection_262

@agnivade
Copy link
Contributor

agnivade commented Apr 2, 2019

@happilymarrieddad - Thank you for voicing your concerns. I can assure you that a lot of thought has went into designing the time package and it's not just a matter of not wanting to "implement the extremely complicated cases."

The term "normalization" is used in various contexts. In this context, it just means dates which overflow or underflow are just adjusted to their future or past dates accordingly. If there is a more apt term for this, please feel free to send a PR so that we can update our docs.

There are various scenarios where such a behavior is helpful. For eg, I want to figure out the last day of any month. Normally, it won't be very straightforward because some of them are 30, some are 31, and then there are leap years, DST adjustments and so on. But if dates are normalized, then I can just add 1 month to the current date, and set the day to 0. This effectively goes back 1 day from the first day of the next month, giving you the last day of the current month.

If d is your current date, then
monthEnd := time.Date(d.Year(), d.Month() + 1, 0, 0, 0, 0, 0, d.Location())

In effect, the Go time package treats dates like a number line, where adding/subtracting an amount flows over to the next month, rather than wrapping around or throwing an error.

@ianlancetaylor
Copy link
Member

@happilymarrieddad I see the examples but I don't know what you are trying to do. What does "I'm trying to go back 1 month from March" mean exactly? What precisely do you mean by "back 1 month from March 30"? To me that does not have a precise meaning. But since we wanted to provide an AddDate method, we had to pick a meaning: go back to February 30, and then figure out what date that is. You want something else, but what is it that you want?

@happilymarrieddad
Copy link
Author

@agnivade No worries. I understand what you are saying.

@ianlancetaylor You keep saying "What is it that you want" despite me giving specific examples. I'll just write my own package and use that.

Thanks everyone!

@cespare
Copy link
Contributor

cespare commented Apr 2, 2019

@happilymarrieddad Ian is asking what you want because you haven't described the behavior you're looking for. Yes, you've given an example, but it's not possible for us to extrapolate from the example to a precise definition. I've read this thread several times now and I also don't know what your preferred meaning of "subtract a month" is.

To put it another way, you'd need to describe why subtracting a month from March 30 should give February 28, just like Agniva and Ian have explained why AddDate behaves as it does. (And what should subtracting a month from March 31 give? What about March 28 and March 29?)

@happilymarrieddad
Copy link
Author

happilymarrieddad commented Apr 2, 2019

@cespare I would expect no more than 1 month to be subtracted. Libraries like Momentjs and the Java language handle it the same. With your example of using March then in all those cases you would get February 28 if you were to subtract unless it was a leap year and then you would get February 29. It doesn't make sense to me to transition 2 months or not transition a month just because the days don't to line up. The problem comes in if you are displaying the month for a customer that something happened. Then you add a month from say January at the end of the month and all of a sudden you are showing March. That would be very confusing. It's fine though. I feel like we just disagree. I also might have a unique use case. I will just finish up this package.

I guess another attempt at describing my meaning of "subtract a month" would be if you pass in (0, -1, 0) and you display the month before and after it's confusing if you get 3 and 3 or 1 and 3 (in the case of adding a month to the end of January). You passed in -1 (1). Why is it not changing the month or transitioning more than 1 month. You guys keep saying the expected behavior but I can't find any documentation where this is expected behavior that you are describing in any other library or language. In fact, it's the opposite.

@vysdel
Copy link

vysdel commented Apr 2, 2019

I can't believe it. Everywhere it's Feb 28 or Feb 29 but go, apparently has its own way.
You can try justify this behavior for sure, but for the end users it doesn't make any sense. I believe anyone would expect to get Feb 28 or Feb 29 on subtracting a month from Mar 30. Not Mar 02 for sure.

@ianlancetaylor

What are you really trying to do?

This led to a rather unpleasant bug for us. Yes, I understand it's our fault, possibly a bad design and/or not enough tests. And yes, I've read the docs. But "AddDate normalizes its result" - I personally would never assume to receive Mar 02 from Mar 30

@ianlancetaylor
Copy link
Member

I'm sorry this seems so frustrating.

The docs for time.Time.AddDate refer to the docs for time.Date, and the docs for time.Date give an explicit example that October 32 normalizes to November 1.

It sounds like you both expect that adding or subtracting a month should always give a result in the adjacent month. The only way that could happen would be if we map February 30 and February 31 both to February 28 (or February 29 in a leap year). That would be possible, though I think different people might find that confusing.

But let's consider the case of adding or subtracting a week, rather than a month. If we add seven days to February 23, we get February 30. Now we have to normalize that. With the rule I suggested above, that normalizes to February 28 (or 29). But I think many people would legitimately find that to be very confusing. The right answer for many people would be March 2 (or 1).

So I don't know how to apply your implied normalization rule consistently for time.Time.AddDate. After all, AddDate permits not just adjusting by month, but adjusting by all sorts of values simultaneously. I do not think that we want a rule that says "if you only adjust the month value, normalize this way, but if you adjust other values, normalize this other way."

Again I'm sorry this was confusing and frustrating. But I hope that now that you do understand how the functions work, you can write code that uses them to do what you want.

@mel3kings
Copy link

anyone curious this seems still a relevant issue, and not just specific to March. below I'm subtracting one month from July 31, and expecting June as a response. but apparently it got normalised and just got July 1. developers would never expect subtracting 1 month from 31st of any month (with previous month just having 30 days) just results to the same month without digging into docus/issues raised.

func Test_SubtractingOneMonth(t *testing.T) {
	julyThirtyOne, _ := time.Parse("2006-01-02", "2019-07-31")
	fmt.Println(julyThirtyOne)
	updatedTime := julyThirtyOne.AddDate(0, -1, 0)
	fmt.Println(updatedTime)
}

this one has been raised since 2015: #13790 (comment)

still the case now as of Aug 2019.

maybe a SubstractDate is function is in order that falls back to a previous day or maybe throw an error to let me know some normalisation occurred?

@ianlancetaylor
Copy link
Member

@mel3kings Please read my immediately preceding comment about adding or subtracting a week rather than a month.

@mel3kings
Copy link

@ianlancetaylor understand that this is working as coded/documented but not as developers expected. I will just have to create a wrapper function that if subtracting one month returns to me the same month then i need to subtract again 1 day.

@ianlancetaylor
Copy link
Member

Don't forget to subtract more than 1 day if it's March 29, 30, or 31.

@Freezerburn
Copy link

Freezerburn commented Jan 29, 2020

@ianlancetaylor In response to your example about modifying a date by a week versus a month: weeks/days are inherently different time units from months. If you're adding a week (7 days) then (from my point of view as a developer using a time library) I would expect that the time library can roll some of those days over into another month, just like I would do if looking at a physical calendar.

On the flip side, if I'm adding a month, I would always expect there to be exactly one month moved forward, and the time library figures out what day should be placed into the new time from that. So February + 1 == March in all circumstances, and March - 1 == February in all circumstances, but the day value is changed to make sense based on all the complexities of time. This behavior is harder to put together because months have so many different "sizes" throughout the year and between years. Years also have different sizes in different years, but I would still expect that adding a year to a time would still give me the same month in the result, but the day can potentially change. (as a fun edge case of why this can be surprising/challenging to developers: I would assume that under the normalization rules of Go, if you add a year to January 31 23:59:59 and a leap second is added at some point between the starting and desired year, you might end up with February 1 in the desired year because Go would add that second and roll over to the next month instead of leaving it at January 31)

Of course, you can't change the behavior of AddDate at this point due to the Go 1 compatibility requirements, but it would be extremely nice if another utility method (or methods, or a different type or something) were added that behaved more like the time libraries in other languages. (Java's Joda-inspired time library and Moment.js for Javascript have been brought up as good examples for creating behavior off of)

@happilymarrieddad
Copy link
Author

How about a compromise and adding a function like AddMonth or something that's specific to only months and does the above behavior? That would solve the compatibility issue while still giving developers what they are asking for and in the comments for the function it can mention that the expected behavior is changing the month by only the value passed in.

@ianlancetaylor
Copy link
Member

By all means go ahead and open a proposal for a new function to add to the time package (https://golang.org/s/proposal). Please be sure to specify precisely how it should behave. Thanks.

@thequailman
Copy link

I ran into something similar to this today while adding a months to 2/29/2020--I get different results when adding months and it's causing problems calculating future dates in my app. I think this is related to this issue.

Here is a playground:
https://play.golang.org/p/4OHcRwxItmY

@ianlancetaylor
Copy link
Member

@thequailman This issue is closed. See all the discussion above, especially #31145 (comment).

@roneelnaidu
Copy link

Understand that this is working as designed. However, worth looking how the excel formula "edate" works and also see https://www.timeanddate.com/date/dateadd.html. Both of which work how I would expect adding / subtracting months to work.

The designed approach causes major issues with financial transactions as adding / subtracting months behaves differently than is "convention".

@davecheney
Copy link
Contributor

@roneelnaidu this issue is closed. Unlike many projects, the Go project does not use GitHub Issues for general discussion or asking questions. GitHub Issues are used for tracking bugs and proposals only.

For asking questions, see:

@golang golang locked and limited conversation to collaborators Jun 29, 2020
Mr1X added a commit to Mr1X/go that referenced this issue Oct 8, 2021
AddDateX returns the time more attention to months then AddDate.
For example, AddDateX(0, 1, 0) applied to 2021-08-31
will returns 2021-09-30.
If the desired month does not have this day,
temporarily set the day as the maximum day of the desired month,
and then process the param days.

Fixes golang#31145
@gopherbot
Copy link
Contributor

Change https://golang.org/cl/354889 mentions this issue: time: add AddDateX more attention to months then AddDate

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.