How do you all generally handle leap days when doing time math? If you're selling a service for one year, are you selling 365 days (02/28/12 - 02/26/13) or do you just give away the leap day for free (02/28/12 - 02/27/13)? Do you pay your salaried employees one day extra on a leap year?
What other leap year bugs have people run into? Generally the libraries I work with (e.g. python's timedelta) don't let you add months or years because of their ambiguity.
While I agree, and I fully expected it to go to March 1st instead, the core of the problem is that dates don't obey basic arithmetic identities. Treating Feb 29 as a zero value is an elegant solution in some ways.
But say 2012-02-29 + 1 year went to 2013-03-01. Then what's 2012-03-01 - 1 year? Does it go to March 1st every time, sometimes ignoring that there's an extra day in between, or does it go back 365 days (March 2nd)?
I suspect the only "solution" is to decide that calendar dates are for human consumption only. If you're doing any calculations, you do them on timestamps, where you can declare a 'year' to be one of 365.256363004, 365.24219, or 365.259636 days (sidereal, tropical, and anomalistic years)[1]. Given the importance of calendars and seasons, you'd probably want to use the middle one, as they all essentially share that length of time as a definition of a year. That way you can just screw the whole leap-year concept entirely.
Of course, then you're left with year-long agreements that expire at odd hours of the night.
Different approaches, different bugs. The seconds from UTC approach results in 1 month from Aug 31st being Sep 1st, and 1 month from Jan 31 being Mar 3rd.
Well, a "month", unqualified, is not really a unit of time. You have to know not only which month you're talking about, but in the case of February, what year it's in, to know exactly how long it is. The same goes for years, if you want them to consist of an integral number of days.
So I think as a matter of API design, Ruby has made a wrong choice by making these operations look like arithmetic on known values. It's too tempting to think that they'll obey the arithmetic identities, when they don't. If you want to have an API function "same date N months later", I have no problem with that at all; then it's much less tempting to think that it's just doing addition.
No need to worry about leap days or anything. The framework takes a date/time such as 2012-02-29 15:00 and calculates that one year later is 2013-02-28 15:00. Similarly, 2012-01-31 15:00 called with .AddMonths(1) returns 2012-02-29 15:00 and calling that with .AddMonths(-1) returns 2012-01-29 15:00.
As long as you understand how your library handles these calculations, trying to do it manually is likely to get you in trouble.
A gotcha to look out for in .NET's implementation is that because the library is adjusting invalid results down to the last day of the month, addition and subtraction operations are not commutative:
new DateTime(2012, 3, 31)
.AddMonths(-1)
.AddMonths(1) ==> 2012-03-29
new DateTime(2012, 3, 31)
.AddMonths(1)
.AddMonths(-1) ==> 2012-03-30
Right, which is why an understanding of how .NET handles the calculations is important. If it's important that you preserve things like "last day of the month" there are libraries[0] which support this and will do date calculations accordingly.
Anyone who's been programming for more than a year knows, or should know, that you don't do time arithmetic directly on the date representation. You convert times to some form that is easy to do arithmetic on, like seconds from 1970-1-1 00:00:00 UTC (the Unix epoch) or 1900-1-1 00:00:00 UTC (the Common Lisp epoch), do all your arithmetic on that, then convert back.
So to add a year, all you have to do is add 365.25 * 24 * 60 * 60.
Don't do that either. December 31, 2012 at 9:00 PM + 365.25 * 24 * 60 * 60 seconds = January 1, 2014 at 3:00 AM. I doubt many people would consider that to be one year later. Let a well-tested library handle the date calculations as you likely haven't thought enough about leap years, leap seconds, daylight savings, historical changes in daylight savings, and so on.
Let a well-tested library handle the date calculations
Yes, that's my point exactly. The conversion from the familiar representation to linear seconds incorporates all of those complications (except leap seconds, which hardly anyone bothers with).
If you don't want to use 365.25, you can write code to figure out whether to use 365 or 366; but in neither case will you have a bug like the one Azure hit where a completely invalid date was generated.
Among all the tasks and problems that seem simple, you'll find Time and date questions to be some of the trickiest. Deriding "noobs" doesn't do anyone any credit.
Comments
How do you all generally handle leap days when doing time math? If you're selling a service for one year, are you selling 365 days (02/28/12 - 02/26/13) or do you just give away the leap day for free (02/28/12 - 02/27/13)? Do you pay your salaried employees one day extra on a leap year?
What other leap year bugs have people run into? Generally the libraries I work with (e.g. python's timedelta) don't let you add months or years because of their ambiguity.
The best approach is to treat Feb 29 as a non-day for purposes of adding months and years to a date, for example in Ruby:
Ugh, that's terrible! A type that doesn't obey basic arithmetic identities -- that's almost certain to result in bugs like the one we're discussing.
While I agree, and I fully expected it to go to March 1st instead, the core of the problem is that dates don't obey basic arithmetic identities. Treating Feb 29 as a zero value is an elegant solution in some ways.
But say 2012-02-29 + 1 year went to 2013-03-01. Then what's 2012-03-01 - 1 year? Does it go to March 1st every time, sometimes ignoring that there's an extra day in between, or does it go back 365 days (March 2nd)?
I suspect the only "solution" is to decide that calendar dates are for human consumption only. If you're doing any calculations, you do them on timestamps, where you can declare a 'year' to be one of 365.256363004, 365.24219, or 365.259636 days (sidereal, tropical, and anomalistic years)[1]. Given the importance of calendars and seasons, you'd probably want to use the middle one, as they all essentially share that length of time as a definition of a year. That way you can just screw the whole leap-year concept entirely.
Of course, then you're left with year-long agreements that expire at odd hours of the night.
[1]: http://en.wikipedia.org/wiki/Year#Sidereal.2C_tropical.2C_an...
Different approaches, different bugs. The seconds from UTC approach results in 1 month from Aug 31st being Sep 1st, and 1 month from Jan 31 being Mar 3rd.
I much prefer spelling out plainly the intention.
Well, a "month", unqualified, is not really a unit of time. You have to know not only which month you're talking about, but in the case of February, what year it's in, to know exactly how long it is. The same goes for years, if you want them to consist of an integral number of days.
So I think as a matter of API design, Ruby has made a wrong choice by making these operations look like arithmetic on known values. It's too tempting to think that they'll obey the arithmetic identities, when they don't. If you want to have an API function "same date N months later", I have no problem with that at all; then it's much less tempting to think that it's just doing addition.
In C#:
No need to worry about leap days or anything. The framework takes a date/time such as 2012-02-29 15:00 and calculates that one year later is 2013-02-28 15:00. Similarly, 2012-01-31 15:00 called with .AddMonths(1) returns 2012-02-29 15:00 and calling that with .AddMonths(-1) returns 2012-01-29 15:00.As long as you understand how your library handles these calculations, trying to do it manually is likely to get you in trouble.
A gotcha to look out for in .NET's implementation is that because the library is adjusting invalid results down to the last day of the month, addition and subtraction operations are not commutative:
Right, which is why an understanding of how .NET handles the calculations is important. If it's important that you preserve things like "last day of the month" there are libraries[0] which support this and will do date calculations accordingly.
[0] For example: http://sourceforge.net/projects/dday-ical/
Anyone who's been programming for more than a year knows, or should know, that you don't do time arithmetic directly on the date representation. You convert times to some form that is easy to do arithmetic on, like seconds from 1970-1-1 00:00:00 UTC (the Unix epoch) or 1900-1-1 00:00:00 UTC (the Common Lisp epoch), do all your arithmetic on that, then convert back.
So to add a year, all you have to do is add 365.25 * 24 * 60 * 60.
This was a stunningly stupid, n00b mistake.
Don't do that either. December 31, 2012 at 9:00 PM + 365.25 * 24 * 60 * 60 seconds = January 1, 2014 at 3:00 AM. I doubt many people would consider that to be one year later. Let a well-tested library handle the date calculations as you likely haven't thought enough about leap years, leap seconds, daylight savings, historical changes in daylight savings, and so on.
Let a well-tested library handle the date calculations
Yes, that's my point exactly. The conversion from the familiar representation to linear seconds incorporates all of those complications (except leap seconds, which hardly anyone bothers with).
If you don't want to use 365.25, you can write code to figure out whether to use 365 or 366; but in neither case will you have a bug like the one Azure hit where a completely invalid date was generated.
Yes,
Among all the tasks and problems that seem simple, you'll find Time and date questions to be some of the trickiest. Deriding "noobs" doesn't do anyone any credit.
You're usually pretty safe if you allow well-tested date and time libraries to do all of the heavy lifting.
I'm keenly aware of the complications. My point is that every other professional programmer should be too.
I stand by my statement: this was a stunningly stupid, n00b mistake. If I had made it, I would say exactly the same thing.