12

I googled for a while and the most commonly used method seems to be

date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

However, this method seems to fail for dates before 1893-04-01

The following test fails on my machine with an outcome of 1893-03-31 instead of 1893-04-01:

@Test
public void testBeforeApril1893() throws ParseException {
    Date date = new SimpleDateFormat("yyyy-MM-dd").parse("1893-04-01");

    System.out.println(date);

    LocalDate localDate2 = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

    System.out.println(localDate2);

    assertEquals(1893, localDate2.getYear());
    assertEquals(4, localDate2.getMonth().getValue());
    assertEquals(1, localDate2.getDayOfMonth());
}

The System.out.prinlns are for me to double check the created dates. I see the following output:

Sun Apr 02 00:00:00 CET 1893
1893-04-02
Sat Apr 01 00:00:00 CET 1893
1893-03-31

For 1400-04-01 I even get an output of 1400-04-09.

Is there any method to convert dates before 1893-04 correctly to LocalDate?

As some helpfully pointed out, the reason for this shift is explained in this question. However, I don't see how I can deduce a correct conversion based on this knowledge.

Hengrui Jiang
  • 861
  • 1
  • 10
  • 23
  • 2
    Can you provide the output of `System.out.println(date.getTime())`? –  Jun 28 '17 at 11:36
  • 2
    And also `System.out.println(TimeZone.getDefault().toZoneId());` –  Jun 28 '17 at 11:43
  • 2
    Beware that your desired conversion from `Date` to `LocalDate` is time zone dependent. If you are sure your `Date` object was supposed to be interpreted in your JVM’s default time zone and in the (proleptic) Gregorian calendar, your method of conversion should be correct. – Ole V.V. Jun 28 '17 at 12:25
  • @Hugo date.getTime(): -2422054800000 zoneId: Europe/Berlin – Hengrui Jiang Jun 28 '17 at 12:36
  • @Ole V.V. the Date is supposed to be interpreted in the default time zone. What do you mean with "is in the proleptic Gregorian calendar?" (btw., I hope it's clear that it is about java.time.LocalDate not Joda) – Hengrui Jiang Jun 28 '17 at 12:47
  • 2
    That’s clear enough, @HengruiJiang, I just figured the problem was the same or closely enough related that you could figure out from the other question and its answers. Calendars have varied during history, and in particular different countries have introduced the Gregorian calendar (that you and I use today) at different points, giving rise to discrepancies about dates. For example, the October revolution in 1917 is named so because it happened on October 25 in the Russian calendar, yet it was on November 7 in the Gregorian calendar. – Ole V.V. Jun 28 '17 at 13:04
  • @Ole V.V. I figured that the linked issue is based on the same problem, that cannot deduce any solution from that... – Hengrui Jiang Jun 28 '17 at 13:10
  • 2
    If you’d care to edit the information that you cannot deduce a solution from the linked question into your question, and maybe explain what you are still missing, I shall be happy to press the ‘reopen’ link. My vote alone won’t reopen the question, though, and it’s often hard to get enough of votes for it, just mentioned so you don’t put your expectations too high. – Ole V.V. Jun 28 '17 at 13:25
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/147831/discussion-between-ole-v-v-and-hengrui-jiang). – Ole V.V. Jun 28 '17 at 14:00

3 Answers3

7

If you're just parsing a String input, it's straighforward:

LocalDate d1 = LocalDate.parse("1893-04-01");
System.out.println(d1); // 1893-04-01
LocalDate d2 = LocalDate.parse("1400-04-01");
System.out.println(d2); // 1400-04-01

The output is:

1893-04-01
1400-04-01


But if you have a java.util.Date object and need to convert it, it's a little bit more complicated.

A java.util.Date contains the number of milliseconds from unix epoch (1970-01-01T00:00Z). So you can say "it's in UTC", but when you print it, the value is "converted" to the system's default timezone (in your case, it's CET). And SimpleDateFormat also uses the default timezone internally (in obscure ways that I must admit I don't fully understand).

In your example, the millis value of -2422054800000 is equivalent to the UTC instant 1893-03-31T23:00:00Z. Checking this value in Europe/Berlin timezone:

System.out.println(Instant.ofEpochMilli(-2422054800000L).atZone(ZoneId.of("Europe/Berlin")));

The output is:

1893-03-31T23:53:28+00:53:28[Europe/Berlin]

Yes, it's very strange, but all places used strange offsets before 1900 - each city had its own local time, before UTC standard took place. That explains why you get 1893-03-31. The Date object prints April 1st probably because the old API (java.util.TimeZone) doesn't have all the offsets history, so it assumes it's +01:00.

One alternative to make this work is to always use UTC as the timezone:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // set UTC to the format
Date date = sdf.parse("1893-04-01");
LocalDate d = date.toInstant().atZone(ZoneOffset.UTC).toLocalDate();
System.out.println(d); // 1893-04-01

This will get the correct local date: 1893-04-01.


But for dates before 1582-10-15, the code above doesn't work. That's the date when the Gregorian Calendar was introduced. Before it, the Julian Calendar was used, and dates before it need an adjustment.

I could do it with the ThreeTen Extra project (an extension of java.time classes, created by the same guy BTW). In the org.threeten.extra.chrono package there are the JulianChronology and JulianDate classes:

// using the same SimpleDateFormat as above (with UTC set)
date = sdf.parse("1400-04-01");
// get julian date from date
JulianDate julianDate = JulianChronology.INSTANCE.date(date.toInstant().atZone(ZoneOffset.UTC));
System.out.println(julianDate); // Julian AD 1400-04-01

The output will be:

Julian AD 1400-04-01

Now we need to convert the JulianDate to a LocalDate. If I do LocalDate.from(julianDate) it converts to Gregorian calendar (and the result is 1400-04-10).

But if you want to create a LocalDate with exactly 1400-04-01, you'll have to do this:

LocalDate converted = LocalDate.of(julianDate.get(ChronoField.YEAR_OF_ERA),
                                   julianDate.get(ChronoField.MONTH_OF_YEAR),
                                   julianDate.get(ChronoField.DAY_OF_MONTH));
System.out.println(converted); // 1400-04-01

The output will be:

1400-04-01

Just be aware that dates before 1582-10-15 have this adjustment and SimpleDateFormat can't handle these cases properly. If you need to work just with 1400-04-01 (year/month/day values), use a LocalDate. But if you need to convert it to a java.util.Date, be aware that it might not be the same date (due to Gregorian/Julian adjustments).


If you don't want to add another dependency, you can also do all the math by hand. I've adapted the code from ThreeTen, but IMO the ideal is to use the API itself (as it can cover corner cases and other things I'm probably missing by just copying a piece of code):

// auxiliary method
public LocalDate ofYearDay(int prolepticYear, int dayOfYear) {
    boolean leap = (prolepticYear % 4) == 0;
    if (dayOfYear == 366 && leap == false) {
        throw new DateTimeException("Invalid date 'DayOfYear 366' as '" + prolepticYear + "' is not a leap year");
    }
    Month moy = Month.of((dayOfYear - 1) / 31 + 1);
    int monthEnd = moy.firstDayOfYear(leap) + moy.length(leap) - 1;
    if (dayOfYear > monthEnd) {
        moy = moy.plus(1);
    }
    int dom = dayOfYear - moy.firstDayOfYear(leap) + 1;
    return LocalDate.of(prolepticYear, moy.getValue(), dom);
}

// sdf with UTC set, as above
Date date = sdf.parse("1400-04-01");
ZonedDateTime z = date.toInstant().atZone(ZoneOffset.UTC);

LocalDate d;
// difference between the ISO and Julian epoch day count
long julianToIso = 719164;
int daysPerCicle = (365 * 4) + 1;
long julianEpochDay = z.toLocalDate().toEpochDay() + julianToIso;
long cycle = Math.floorDiv(julianEpochDay, daysPerCicle);
long daysInCycle = Math.floorMod(julianEpochDay, daysPerCicle);
if (daysInCycle == daysPerCicle - 1) {
    int year = (int) ((cycle * 4 + 3) + 1);
    d = ofYearDay(year, 366);
} else {
    int year = (int) ((cycle * 4 + daysInCycle / 365) + 1);
    int doy = (int) ((daysInCycle % 365) + 1);
    d = ofYearDay(year, doy);
}
System.out.println(d); // 1400-04-01

The output will be:

1400-04-01

Just reminding that all this math is not needed for dates after 1582-10-15.


Anyway, if you have an input String and want to parse it, don't use SimpleDateFormat - you can use LocalDate.parse() instead. Or LocalDate.of(year, month, day) if you already know the values.

But converting these local dates from/to a java.util.Date is more complicated, because Date represents the full timestamp millis and dates can vary according to the calendar system in use.

  • 1
    I am actually not trying to parse a String input, but convert an existing Date to LocalDate. I only used the String SimpleDateFormat to create a Date for the test – Hengrui Jiang Jun 28 '17 at 12:43
  • @HengruiJiang I also explain how to convert it, using `JulianChronology`. –  Jun 28 '17 at 13:04
  • 1
    Is there any solution that do not include additional dependencies? I would really prefer not having to add any dependencies... – Hengrui Jiang Jun 28 '17 at 13:08
  • @HengruiJiang I've updated the answer with a solution without dependencies. But I still think that adding this dependency is better, because I might be missing some detail (I've just adapted some of the API's code, but not sure if it'll work for all cases you need) –  Jun 28 '17 at 13:32
  • Thanks very much! As far as I understood correctly, I could also convert to Gregorian Calendar and then create a new LocalDate with the year, month and day, if I don't have to deal with dates before 1583? That would be good enough in my case – Hengrui Jiang Jun 28 '17 at 13:41
  • 2
    For dates before 1583-10-15, you do this conversion from Gregorian to Julian. For dates after that, you don't need to do all that math (just work with UTC as explained). –  Jun 28 '17 at 13:45
3

Seems to be a known bug that won't get fixed: https://bugs.openjdk.java.net/browse/JDK-8061577

After a lot of research I gave up with every simple API method and just convert it by hand. You could wrap the date in a sql.Date and call toLocalDate() or you just use the same deprecated methods as sql.Date does. Without deprecated methods you need to convert your util.Date to Calendar and get the fields one by one:

    Calendar calendar = Calendar.getInstance();
    calendar.setTime(value);
    return LocalDate.of(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1,
                        calendar.get(Calendar.DAY_OF_MONTH));

If you futher want to have a two digit year conversion like in SimpleDateFormat (convert the date in range of now - 80 years till now + 19 years) you could use this implementation:

    Calendar calendar = Calendar.getInstance();
    calendar.setTime(value);
    int year = calendar.get(Calendar.YEAR);
    if (year <= 99) {
        LocalDate pivotLocalDate = LocalDate.now().minusYears(80);
        int pivotYearOfCentury = pivotLocalDate.getYear() % 100;
        int pivotCentury = pivotLocalDate.minusYears(pivotYearOfCentury).getYear();
        if (year < pivotYearOfCentury) {
            year += 100;
        }
        year += pivotCentury;
    }
    return LocalDate.of(year, calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH));

Conclusion: it is realy ugly and I can't believe that there isn't any simple API!

cornz
  • 641
  • 4
  • 18
0

This code works for me:

@Test
public void oldDate() throws ParseException {
    Date date = new SimpleDateFormat("yyyy-MM-dd").parse("1893-04-01");
    assertEquals("1893-04-01", String.format("%tF", date));
}
Artem Lukanin
  • 556
  • 3
  • 15