1

I built a small function to convert a TimeStamp into a LocalDate, and stumbled upon strange behaviour. Here is the code:

import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class Test {

  private static void convert(long seconds) {
    System.out.printf("At %d seconds%n", seconds);
    Timestamp t = new Timestamp(seconds * 1000L);
    System.out.format("Timestamp: %s%n", t);
    Instant i = t.toInstant();
    System.out.format("Instant:   %s%n", i);
    ZonedDateTime atZone = i.atZone(ZoneId.systemDefault());
    System.out.format("at Zone:   %s%n", atZone);
    LocalDate localDate = atZone.toLocalDate();
    System.out.format("LocalDate: %s%n", localDate);
  }
}

My local timezone is Europe/Berlin. Now, when calling this for Timestamp 0, I get the correct result:

At 0 seconds
Timestamp: 1970-01-01 01:00:00.0
Instant:   1970-01-01T00:00:00Z
at Zone:   1970-01-01T01:00+01:00[Europe/Berlin]
LocalDate: 1970-01-01

Everything is as expected. But when I call it with a date in the year 1, it fails my expectations:

At -62135769600 seconds
Timestamp: 0001-01-01 01:00:00.0
Instant:   0000-12-30T00:00:00Z
at Zone:   0000-12-30T00:53:28+00:53:28[Europe/Berlin]
LocalDate: 0000-12-30

Questions:

  1. There is a missing day after converting to Instant, so it seems like the timezones UTC and Europe/Berlin were a day plus one hour apart at that time?
  2. When converting the Instant back to my timezone, 6 minutes and 32 seconds get lost somewhere! Why is that?
  3. The resulting LocalDate is not correct. What would be a robust way to do the conversion?
5
  • Just out of interest: What are you trying to calculate so close After Christ? As statetd in the JavaDocs of java.sql.Timestamp, it is a thin wrapper around java.util.Date but also that it is recommended that code not view Timestamp values generically as an instance of java.util.Date. Best practice: Don't use anything related to java.util.Date for calculations on dates and times anymore. This tells you why... Commented Feb 27, 2020 at 9:15
  • @deHaar: I am trying to connect to a legacy backend, which, in the case it wants to return a NULL date returns 0001-01-01as a timestamp instead, and I want to write a converter to LocalDate that would handle that case and return NULL. Commented Feb 27, 2020 at 9:40
  • Couldn't you then just perform checks for LocalDate.getYear() > 1 or similar? I mean always convert the timestamp and check the resulting LocalDate for valid values. Commented Feb 27, 2020 at 9:48
  • Sure, many workarounds are possible, I just want to understand what's happening! :) Commented Feb 27, 2020 at 10:06
  • 2
    Well, me too ;-) I have checked if the year 0 is considered a leap year, and it seems like it is... Could be (one of) the reason(s) for a weird amount of seconds to be subtracted... I think java.time is not perfectly designed for dates and times around Christ. In addition, there shouldn't be a year 0, but only a year 1 B.C. immediately followed by a year 1 A.C... Not entirely sure about that. Commented Feb 27, 2020 at 10:25

2 Answers 2

1

I've encountered this issue with I tried to switch from XMLGregorianCalendar over to java.time.ZonedDateTime and our system depended on Year 1 to represent a 'minimum' date. Things get very weird once you start converting between dates before the Julian-Gregorian transition. e.g. Year 201 Year 200

Here's the Javadoc snippet that hints at things going a bit funny in the early years

GregorianCalendar implements proleptic Gregorian and Julian calendars. That is, dates are computed by extrapolating the current rules indefinitely far backward and forward in time. As a result, GregorianCalendar may be used for all years to generate meaningful and consistent results. However, dates obtained using GregorianCalendar are historically accurate only from March 1, 4 AD onward, when modern Julian calendar rules were adopted. Before this date, leap year rules were applied irregularly, and before 45 BC the Julian calendar did not even exist.

https://docs.oracle.com/javase/8/docs/api/java/util/GregorianCalendar.html

If you open up the source of GregorianCalendar, you find another lovely 'Implementation note' that suggests even more craziness at play:

 * Likewise, with the Julian calendar, we assume a consistent
 * 4-year leap year rule, even though the historical pattern of
 * leap years is irregular, being every 3 years from 45 BCE
 * through 9 BCE, then every 4 years from 8 CE onwards, with no
 * leap years in-between.  Thus date computations and functions
 * such as isLeapYear() are not intended to be historically
 * accurate.

I've written a date-conversion library that I believe implements the conversion between date types in an 'expected' way: https://github.com/beirtipol/date-converters . I'd hoped to be able to provide a consistent conversion between java.util and java.time dates pre 1582 (Julian->Gregorian shift) but it's not complete.

To answer part of your question, I'm not sure there is a robust way of doing this without testing every single date and timezone pre-1582. I'm attempting to do this and will post back if I figure it out, but there's a long road of testing ahead!

Here's some more information on the joy of Roman-Julian-Gregorian shifts: https://www.timeanddate.com/calendar/julian-calendar.html

Sign up to request clarification or add additional context in comments.

Comments

0

The problem seems to be caused by your input parameter to convert function. The "At -62135769600 seconds" shows that your input value exceeds the singed long limits. Below is a test output for input value representing a timestamp from year one (370 * 24 * 60 * 60).

At 31968000 seconds
Timestamp: 1971-01-06 01:00:00.0
Instant:   1971-01-06T00:00:00Z
at Zone:   1971-01-06T01:00+01:00[Europe/Berlin]
LocalDate: 1971-01-06

2 Comments

Nope, Long.MIN_VALUE == -9223372036854775808L, while my timestamp is -62135769600L * 1000L == -62135769600000L which is well in range.
I misunderstood the question (year 1 part), sorry.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.