Reputation: 881
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.prinln
s 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.
Upvotes: 12
Views: 2254
Reputation: 556
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));
}
Upvotes: 0
Reputation:
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.
Upvotes: 7
Reputation: 641
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!
Upvotes: 3