1

I try to provide a tool to convert datetime from Java to C#. But there is a serious problem.

In Java, I read '0001-01-01' from the SQL Server database via java.sql.Date, and get the millisecond -62135798400000.

I also consider the timezone offset.

private static long getMilliSecondWithoutTimeZone(long origin) {
    return origin + (ZonedDateTime.now().getOffset().getLong(OFFSET_SECONDS) * 1000);
}

And the final millisecond is -62135769600000.

In C#, I use this millisecond to new Datetime

var ticks = new DateTime(1970, 1, 1).Ticks + (-62135769600000 * 10000);
var date = new DateTime(ticks);

When the code runs, it will throw the exception:

System.ArgumentOutOfRangeException: 'Ticks must be between DateTime.MinValue.Ticks and DateTime.MaxValue.Ticks. (Parameter 'ticks')'

However, the conversion is correct after '1600-01-01' according to my test.
Before '1600-01-01', there always is a few days of error.

It makes me very confused.


I find the remarks in https://learn.microsoft.com/en-us/dotnet/api/system.globalization.juliancalendar?view=net-5.0#remarks

The Gregorian calendar was developed as a replacement for the Julian calendar (which is represented by the JulianCalendar class) and was first introduced in a small number of cultures on October 15, 1582. When working with historic dates that precede a culture's adoption of the Gregorian calendar, you should use the original calendar if it is available in the .NET Framework. For example, Denmark changed from the Julian calendar to the Gregorian calendar on February 19 (in the Julian calendar) or March 1 (in the Gregorian calendar) of 1700. In this case, for dates before the adoption of the Gregorian calendar, you should use the Julian calendar. However, note that no culture offers intrinsic support for the JulianCalendar class. You must use the JulianCalendar class as a standalone calendar. For more information, see Working with calendars.

The actual reason is:

  • C# uses the Gregorian calendar all the time.
  • Java uses the Gregorian calendar after October 15, 1582, and uses the Julian calendar before.

The solution:

import java.sql.Date;
import java.time.chrono.IsoChronology;
import java.time.*;

public class Test {
    public static Long getMilliSeconds(Date date) {
        if (null == date) {
            return null;
        }
        IsoChronology ISO = IsoChronology.INSTANCE;
        LocalDate ld = date.toLocalDate();
        return ISO.localDateTime(LocalDateTime.of(ld.getYear(), ld.getMonth(), ld.getDayOfMonth(), 0, 0, 0)).toInstant(ZoneOffset.UTC).toEpochMilli();
    }
}
Shawn
  • 25
  • 6

2 Answers2

1

It seems like the millisecond value that you mention, -62_135_798_400_000, comes out of an old-fashioned java.sql.Date object created in a timezone that is assumed to be at UTC offset +08:00 back then, perhaps just Etc/GMT-8. With this assumption, the value is historically correct since it was the Julian calendar that was used back then, and Date does use that.

I don’t know the .NET classes that C# uses, but I consider it a likely that a few days error are caused by them using the proleptic Gregorian calendar, that is, pretending that the Gregorian calendar was used in all past even though it didn’t come into existence before 1582. The modern Java date and time API does this and therefore gives you millisecond values that usually differ by a few days.

    long milliseconds = LocalDate.of(1, 1, 1)
            .atStartOfDay(ZoneOffset.ofHours(8))
            .toInstant()
            .toEpochMilli();
    System.out.format(Locale.ENGLISH, "%,d%n", milliseconds);

Output:

-62,135,625,600,000

It is 48 hours — or 2 days — later than the time you mentioned. See if it solves your issue.

Link

Oracle tutorial: Date Time explaining how to use java.time.

Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • 1
    I find the remarks in Microsoft Document. https://learn.microsoft.com/en-us/dotnet/api/system.globalization.gregoriancalendar?view=net-5.0#remarks – Shawn Jan 28 '21 at 05:59
  • Thanks for your answer. It can not solve my issue, but according to the idea you provide, using IsoChronology can solve it. – Shawn Jan 29 '21 at 01:48
  • `LocalDate` is using the ISO chronology, so I don’t immediately think that involving [`IsoChronology`](https://docs.oracle.com/javase/10/docs/api/java/time/chrono/IsoChronology.html) would change anything. Let us know if you find out that it can. – Ole V.V. Jan 29 '21 at 15:33
  • The solution has been added to the question body. – Shawn Jan 31 '21 at 06:44
0

You forgot to account for time zone offset.

If we set the time zone to UTC, you'll see this:

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
System.out.println(new Date(-62135798400000L));

Output

Fri Dec 31 16:00:00 UTC 1

It is actually year 1 BC, not year 1 AD.

The time 16:00 indicates a time zone offset of 8 hours, so if we change to GMT+8 we get:

TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
System.out.println(new Date(-62135798400000L));

Output

Sat Jan 01 00:00:00 GMT+08:00 1

That is correctly year 1 AD.

Which means that you need to adjust the millisecond value by 8 hours, aka 28800000 milliseconds.

For the date 0001-01-01 00:00 UTC, the correct value for milliseconds is -62135769600000. Anything less than that will be rejected by the C# DateTime class.

Andreas
  • 154,647
  • 11
  • 152
  • 247
  • Yes, the time zone offset needs to be considered. But even so, the result is still wrong in C#. The same exception will be thrown – Shawn Jan 28 '21 at 05:12