7

I'm getting inconsistent results when converting Dates to LocalDates, around the year 200. Using the following code to do the conversion:

  private LocalDate toLocalDate(Date localDate)
  {
    return LocalDateTime.ofInstant(localDate.toInstant(), ZoneId.systemDefault()).toLocalDate();
  }

My ZoneId.systemDefault() is Africa/Harare, which matches the CAT used in the test. The test case I run is

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);
String dateString = "Tue Jan 01 00:00:00 CAT 200";
String dateString2 = "Tue Jan 01 00:00:00 CAT 201";
String dateString3 = "Wed Dec 31 00:00:00 CAT 200";

System.out.println(toLocalDate(simpleDateFormat.parse(dateString)));
System.out.println(toLocalDate(simpleDateFormat.parse(dateString2)));
System.out.println(toLocalDate(simpleDateFormat.parse(dateString3)));

My expected output for this would be

0200-01-01
0201-01-01
0200-12-31

Or, if not that, at least consistently incorrect values. The actual results are

0199-12-31
0201-01-01
0200-12-31

So it seems that the first one is being rolled back slightly, possibly the two hours corresponding to the CAT timezone? But why does this only happen on the one case? Doing the same experiment with the year 2000 does not produce the same error.

assylias
  • 321,522
  • 82
  • 660
  • 783
Evan Knowles
  • 7,426
  • 2
  • 37
  • 71
  • 5
    Side note - using a name of `localDate` for a variable of type `Date` is quite confusing, given the existence of the type `LocalDate` with a very different meaning. – Jon Skeet Oct 01 '15 at 07:57
  • Did Africa/Harare maybe change the time zone around 200? – Puce Oct 01 '15 at 07:57
  • @JonSkeet It is - my bad, this code has been tossed and turned a couple of times whilst looking at this :) Puce I don't think so - I get weird results with some other years too, didn't want to put too many examples in though. – Evan Knowles Oct 01 '15 at 07:58
  • 1
    If nothing else, the day names are broken - if December 31st 200 is a Wednesday, how can January 1st 201 be a Tuesday? – Jon Skeet Oct 01 '15 at 07:59
  • 2
    @Puce you realize the concept of a time zone didn't exist in 200AD. – RealSkeptic Oct 01 '15 at 07:59
  • @JonSkeet - excellent point! The weird thing is, I copied these off of a Date `toString` result. I'll go check that toString again. – Evan Knowles Oct 01 '15 at 08:00
  • 2
    Note that CAT *isn't* the same as Africa/Harare - CAT is being treated as "UTC+2" whereas Africa/Harare has an offset of +02:10:20 back then. – Jon Skeet Oct 01 '15 at 08:01
  • @JonSkeet That would explain some of the weirdness I see during the calculation, but shouldn't it be consistently wrong? – Evan Knowles Oct 01 '15 at 08:02
  • 2
    Yes, it should. Basically as far as I can see there are *three* bad things here - the day name, the assumption of CAT == Africa/Harare, and a Java 8 bug. I'm trying to isolate it to *just* the bug, at which point I'll post an answer. – Jon Skeet Oct 01 '15 at 08:03

1 Answers1

6

Stephen has provided an explanation in the comment. Basically, java.util.Date uses a calendar system which cuts over between the Julian calendar system and the Gregorian calendar system in 1582, skipping 10 days. So dates in 1582 or before will exhibit discrepancies - but the size of the discrepancy will vary over time - by 3 days every 400 years, on average. It so happens that between 200 and 400AD, you don't see this because that corresponds to when the discrepancy is 0.

Here's a short but complete program to demonstrate the problem:

import java.time.*;
import java.util.*;

public class Test {
    public static void main(String[] args) throws Exception {
        // Value obtained with Noda Time: should be 0199-12-31T22:00:00Z.
        long millis = -55855792800000L;
        Instant instant = Instant.ofEpochMilli(millis);
        Date date = new Date(millis);
        System.out.println(instant);
        System.out.println(date);
    }
}

Output on my machine:

0199-12-31T22:00:00Z
Tue Jan 01 22:00:00 GMT 200

This is all complicated by the problems in your initial code of assuming CAT and Africa/Harare are the same (at that point in time, Africa/Harare is regarded as having an offset of +02:10) and the incorrect day names in your strings - but it's the bug in Java which is causing the issue here.

I suggest you perform all your parsing using the java.time.format classes - then I'd hope you won't get this inconsistency.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • The code that originally led me to find this was from moving back and forth between `Date` and `LocalDate` whilst trying to update code to use new features, so there was no parsing there unfortunately - it ends up using `Date.from(instant)`, which uses the equivalent of this same constructor. I'll look for another way of converting it, thanks. – Evan Knowles Oct 01 '15 at 08:26
  • As per this answer: http://stackoverflow.com/questions/22929237/convert-java-time-localdate-into-java-util-date-type this appears to be the standard way to convert between date types, which is inconvenient. – Evan Knowles Oct 01 '15 at 08:28
  • 3
    Java 8 uses the ISO calendar system which has no Gregorian cutover, whereas java.util.Date uses the standard cutover of 10 days in 1582. Jan 1st 200 is before an extra leap day (Feb 29th 200), so is one day earlier. – JodaStephen Oct 01 '15 at 09:27
  • 2
    @JodaStephen: Thanks - so basically dates prior to 1582 gradually slide further away from each other as you go back in history, if you compare java.util.Date and java.time.*? Will update. – Jon Skeet Oct 01 '15 at 09:28