Friday, September 18, 2009

Year 0, Month 0, Day 0

Come with me, if you so desire, for a deep dive into Java date/time manipulation.

Due to a perhaps fortunate accident, we found ourselves looking at a strange behavior in a Derby test suite, which ended up boiled down to the following question: how should this code behave:


System.out.println(Timestamp.valueOf("0000-00-00 15:47:28.0"));


Now, there is no such thing as Year 0, nor Day 0, nor Month 0, so you might think that this code might throw an exception and refuse the date as illegal. But it doesn't! Instead, it prints:

0002-11-30 15:47:28.0

Now, this is strange in several ways:

  • Why is the year printed as year 2?

  • Why is the month and day printed as November 30?



Now, it turns out that the java.sql.Timestamp class is built atop java.sql.Date, which is built atop java.util.Date, which is built atop java.util.Calendar. So now we might wonder about how this code uses Calendar, and, in particular, about what the following code might do:


Calendar c = Calendar.getInstance();
c.set(0, 0, 0, 12, 13, 14);
System.out.println(c.getTime());


It turns out that this code prints:

Wed Dec 31 12:13:14 PST 0002

Well, that is very interesting!

It seems logical, somewhat, to say that day 0 of month 0 might be December 31. But why did the code print year 2?

It turns out that this is a known feature of the Calendar implementation:


GregorianCalendar represents a date with ERA and YEAR. 0 and negative year values are converted to (1 - year) with an ERA change to support the Julian calendar year numbering.


So:

  • Year 0 of ERA 1 is converted to year -1 of ERA 0, because the year before 1 AD was 1 BC (there really was no year 0 in this system)

  • But then year -1 in ERA 0 is stored as (1 - year) which is thus (1 - -1) which is 2



OK, so that explains the year 2 business. But how did we get November 30 in our output? Shouldn't we have had December 31?

Well, it turns out that Timestamp.valueOf() uses the (now obsolete) Timestamp constructor, which takes separate year, month, day, hour, minute, second, and nanosecond values.

This constructor, however, expects that while the date counts from 1, and is thus in the range 1-31, the month counts from 0, and thus should be in the range 0-11.

So Timestamp.valueOf takes the month portion of the string that it was provided and subtracts 1 from it, so that, e.g., 2009-09-18 gets converted into month 8, date 18.

So that meant that by the time we got to building a Calendar value, internally, we had a month value of -1, not 0, and so we were effectively running this code:


c.set(0, -1, 0, 12, 13, 14);
System.out.println(c.getTime());


And this code does indeed print:

Sun Nov 30 12:13:14 PST 0002

Here's the entire program: run it yourself on your copy of Java and see what it does!


import java.util.Calendar;
import java.sql.Timestamp;
public class Test
{
public static void main(String []args)
{
System.out.println(Timestamp.valueOf("0000-00-00 15:47:28.0"));

Calendar c = Calendar.getInstance();
c.set(0, 0, 0, 12, 13, 14);
System.out.println(c.getTime());

c = Calendar.getInstance();
c.set(0, -1, 0, 12, 13, 14);
System.out.println(c.getTime());
}
}


We now return you to your normal day, month, and year.

No comments:

Post a Comment