6

As some game developing might use the date time which is in the past or in the future, and in any time zone of a date time emulation, even we never reach them in reality. For this assumption, not saying that I'm calculating on fake date times, but for the correctness of calculation on any date time in any time zone like it was in reality.

I previously asked a question about the time zone issue of China in Java, which was regarded as a duplicate question, and I so deleted it. However, from this comment thread, I'm aware that it's some kind of time rewind(transition?) issue in Java, not just about the time zone change.

And now, I re-post this question in a different manner, to present this issue with the following:

  • Code in Java

    import org.joda.time.*;
    import java.util.*;
    
    class PandaTest {
        static long Subtract(
            Date minuend, Date subtrahend, DateTimeZone zone) {
            long millis;
    
            if(null==zone)
                millis=minuend.getTime()-subtrahend.getTime();
            else {
                long rhs=
                    (new LocalDateTime(subtrahend)).toDateTime(zone)
                    .getMillis();
    
                long lhs=
                    (new LocalDateTime(minuend)).toDateTime(zone)
                    .getMillis();
    
                millis=lhs-rhs;
            }
    
            return millis/1000;
        }
    
        static Date MakeTime(
            int year, int month, int day, int hour, int minute, int second
            ) {
            Calendar calendar=
                Calendar.getInstance(TimeZone.getTimeZone("PRC"));
    
            calendar.set(year, month-1, day, hour, minute, second);
            return calendar.getTime();
        }
    
        static void puts(String arg0) {
            System.out.println(arg0);
        }
    
        static void ShowDuration(DateTimeZone zone, Date ... args) {
            int argc=args.length;
    
            puts("--- ");
            puts("Time Zone: "+(null!=zone?zone.toString():"unspecified"));
    
            for(int i=0; argc-->0; ++i) {
                puts("Time "+i+": "+args[i]);
    
                if(i>0) {
                    long duration=Subtract(args[i], args[i-1], zone);
                    puts("Duration = "+duration);
                }
            }
        }
    
        public static void main(String[] args) {
            Date retainsOfTheDay=MakeTime(1900, 1, 1, 8, 5, 51+0);
            Date somewhereInTime=MakeTime(1900, 1, 1, 8, 5, 51+1);
            DateTimeZone zone=DateTimeZone.forID("PRC");
            ShowDuration(null, retainsOfTheDay, somewhereInTime);
            ShowDuration(zone, retainsOfTheDay, somewhereInTime);
        }
    }
    

The problem occurs if I constructed a LocalDateTime of JodaTime from a Date of Java. The version of JDK is 7u17 and JodaTime is 2.2. It doesn't happen in C# with NodaTime, I put an alternative version of code at the rear of this post for contrasting.

  • Question

    1. How the transition occurs, is it exact as the Unix Epoch?

      I possibly use the term transition in a wrong way. What I mean is the strange result of subtracting 1900/1/1 8:5:52 by 1900/1/1 8:5:51 in Java. There isn't a time zone change at that time.

      Does something like this only occur in the specific time zone, or all of the time zones(maybe at some different instant)?

    2. If one would possibly calculate on arbitrary date times in any time zone expecting the result would always be correct, should Date and Calendar be used?

      If yes, how to use them without the issue occurs?

      Should we no longer to use Date and Calender in Java, once we possibly calculate on date times before 1970 or after 2038?


Alternative Code

The code contains content both in C# and Java, that we can contrast the result in C# and Java conveniently:

// Like Java, like Sharp ... the code contains content either in Java or C# 
// An odd number of `slash-star-slash` at the beginning to compile in Java
// An even number of `slash-star-slash` at the beginning to compile in C#
// p.s.: zero would be treated as an even number

using Date=System.DateTime;
using NodaTime.TimeZones;
using NodaTime;
using System.Collections.Generic;
using System.Linq;
using System; /*/
import org.joda.time.*;
import java.util.*;

// ClockCant in Java 
class ClockCant {
    public static Date MakeTime(
        int year, int month, int day, int hour, int minute, int second
        ) {
        Calendar calendar=
            Calendar.getInstance(TimeZone.getTimeZone("PRC"));

        calendar.set(year, month-1, day, hour, minute, second);
        return calendar.getTime();
    }

    public static DateTimeZone GetZoneFromId(String id) {
        return DateTimeZone.forID(id);
    }

    public static String GetYourZoneId() {
        return DateTimeZone.getDefault().getID();
    }

    public static long Subtract(
        Date minuend, Date subtrahend, DateTimeZone zone) {
        long millis;

        if(null==zone)
            millis=minuend.getTime()-subtrahend.getTime();
        else {
            long rhs=
                (new LocalDateTime(subtrahend)).toDateTime(zone)
                .getMillis();

            long lhs=
                (new LocalDateTime(minuend)).toDateTime(zone)
                .getMillis();

            millis=lhs-rhs;
        }

        return millis/1000;
    }
}

// a minimum implementation of C#-like List<T> for Java
class List<T> {
    public T[] ToArray() {
        return _items;
    }

    public int Count() {
        return _items.length;
    }

    public List(T ... args) {
        _items=args;
    }

    T[] _items;
}

/*/// ClockCant in C#
class ClockCant {
    public static Date MakeTime(
        int year, int month, int day, int hour, int minute, int second) {
        return new Date(year, month, day, hour, minute, second);
    }

    public static DateTimeZone GetZoneFromId(String id) {
        return DateTimeZoneProviders.Tzdb[id];
    }

    public static String GetYourZoneId() {
        return DateTimeZoneProviders.Tzdb.GetSystemDefault().Id;
    }

    public static long Subtract(
        Date minuend, Date subtrahend, DateTimeZone zone) {
        long ticks;

        if(null==zone)
            ticks=minuend.Subtract(subtrahend).Ticks;
        else {
            var rhs=
                LocalDateTime.FromDateTime(subtrahend)
                .InZoneLeniently(zone);

            var lhs=
                LocalDateTime.FromDateTime(minuend)
                .InZoneLeniently(zone);

            ticks=(lhs.ToInstant()-rhs.ToInstant()).Ticks;
        }

        return ticks/TimeSpan.TicksPerSecond;
    }
}

// extension for Java-like methods in C#
static partial class JavaExtensions {
    public static String toString(this Object x) {
        return x.ToString();
    }
}

class PandaTest { /*/ class PandaTest {
    // in Java
    public static void main(String[] args) {
        Language="Java";
        Main(args);
    }

    static void puts(String arg0) {
        System.out.println(arg0);
    }

    static void ShowDuration(DateTimeZone zone, Date ... args) {
        ShowDuration(zone, new List<Date>(args));
    }

    /*/// in C#
    static void puts(String arg0) {
        Console.WriteLine(arg0);
    }

    static void ShowDuration(DateTimeZone zone, params Date[] args) {
        ShowDuration(zone, args.ToList());
    }

    /**/// the following code are for both C# and Java
    static void ShowDuration(DateTimeZone zone, List<Date> args) {
        int argc=args.Count();
        Date[] argv=args.ToArray();

        puts("--- ");
        puts("Time Zone: "+(null!=zone?zone.toString():"unspecified"));

        for(int i=0; argc-->0; ++i) {
            puts("Time "+i+": "+argv[i]);

            if(i>0) {
                long duration=ClockCant.Subtract(argv[i], argv[i-1], zone);
                puts("Duration = "+duration);
            }
        }
    }

    static void Main(String[] args) {
        Date retainsOfTheDay=ClockCant.MakeTime(1900, 1, 1, 8, 5, 51+0);
        Date somewhereInTime=ClockCant.MakeTime(1900, 1, 1, 8, 5, 51+1);
        DateTimeZone zone=ClockCant.GetZoneFromId("PRC");
        puts("Current Time Zone: "+ClockCant.GetYourZoneId());
        puts("Language: "+Language);
        ShowDuration(null, retainsOfTheDay, somewhereInTime);
        ShowDuration(zone, retainsOfTheDay, somewhereInTime);
    }

    static String Language="C#";
}

To compile in Java, add a /*/ at the beginning of the code as the following:

/*/// Like Java, like Sharp ... 
Community
  • 1
  • 1
Ken Kin
  • 4,503
  • 3
  • 38
  • 76
  • 21
    There's a meme to be made in this title. – Grant Thomas Apr 15 '13 at 10:34
  • 1
    I think an earlier comment I made has confused things a little. The "extra" transition point is at the start of 1900 (UTC), not at the start of 1970 (UTC) which is when the Unix epoch occurs. Your question is a bit unclear, to be honest - you talk about "the transition" but most time zones have lots of transitions. Is the issue *just* that Java has decided that all time zones should basically be "standard time only" before 1900? – Jon Skeet Apr 15 '13 at 10:38
  • @GrantThomas: Meme? Do you mean **mimic**? – Ken Kin Apr 15 '13 at 10:38
  • 1
    @KenKin: No, meme. But in general, I think this question could be made a lot clearer by presenting separate (and almost certainly shorter) Java code and C# code, and not talking about the 1927 transition at all (which seems to be a red herring). Likewise things like the derivation for the name of the test are irrelevant. – Jon Skeet Apr 15 '13 at 10:39
  • 4
    @GrantThomas re: meme - JodaTime is dangerously close to YodaTime... =) – J. Steen Apr 15 '13 at 10:47
  • 1
    @KenKin - What do you mean by "transition"? Do you mean the changes to "daylight saving time" and back? Do you mean changes in epoch, like in happens in Japan? Do you mean when they actually changed the calendar system (like in the Julian to Gregorian calendar change that happened in the 1750's ... http://www.cslib.org/CalendarChange.htm)? – Stephen C Apr 15 '13 at 11:04
  • @KenKin - If you mean daylight savings transitions, then ... obviously there are two transitions every year. When DST starts ... and when it ends. – Stephen C Apr 15 '13 at 11:06
  • 5
    Yep, my brain totally read this question's title in Yoda's voice. "When time zone of China it goes, work as good Java will not, hmmm? Yes?" – neminem Apr 18 '13 at 22:51
  • Why are only the [tag:nodatime] and [tag:jodatime] tags?. Why isn't there a [tag:yodatime] tag? – user93353 Apr 24 '13 at 11:22
  • 1
    @GrantThomas Here is the Yoda Meme: http://memegenerator.net/instance/37263978 – Jeremy Thompson Apr 25 '13 at 06:46
  • @JeremyThompson: Wow .. – Ken Kin Apr 25 '13 at 11:59

1 Answers1

9

I believe your Joda Time test is broken because you've made it unnecessarily complex, basically. Here's a simple short but complete program demonstrating the difference:

import java.util.*;
import org.joda.time.*;

public class ChinaTest {
    public static void main(String[] args) {
        DateTime startOf1900 = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeZone.UTC);
        DateTime endOf1899 = startOf1900.minusMillis(1);

        DateTimeZone jodaZone = DateTimeZone.forID("Asia/Shanghai");
        System.out.println("Joda at start of 1900: " +
                           jodaZone.getOffset(startOf1900));
        System.out.println("Joda at end of 1899: " +
                           jodaZone.getOffset(endOf1899));
        TimeZone javaZone = TimeZone.getTimeZone("Asia/Shanghai");
        System.out.println("Java at start of 1900: " + 
                           javaZone.getOffset(startOf1900.getMillis()));
        System.out.println("Java at end of 1899: " + 
                           javaZone.getOffset(startOf1900.getMillis() - 1));
    }
}

Output:

Joda at start of 1900: 29152000
Joda at end of 1899: 29152000
Java at start of 1900: 29152000
Java at end of 1899: 28800000

So basically, Java's time zone thinks there's a transition at the start of 1900, whereas Joda Time doesn't.

As I've written previously, Java's time zone implementation basically assumes that daylight saving time didn't exist before the start of 1900 UTC - so in any time zone where daylight saving time is in effect at the start of 1900, that represents a transition.

Neither Joda Time nor Noda Time make this assumption, which is why you see a difference.

This has nothing to do with the Unix epoch, nor 2038. All it means is that you should expect java.util.TimeZone to treat any date/time before the start of 1900 UTC to be in "standard time" for that time zone.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Thank you very much! So that would mean we should not use `Date` or `Calendar` in Java once we might calculate on the datetime start of 1900, right? Btw, you made the answer a CW post .. – Ken Kin Apr 17 '13 at 13:26
  • 1
    The *question* is CW because it's been edited so much. My answer was therefore community wiki by default. But as for not using Date/Calendar before 1900 - `Date` doesn't have a time zone component anyway, and I very much doubt that many users are going to know or care about DST before 1900. But heck, use Joda Time anyway as a better Java API... – Jon Skeet Apr 17 '13 at 13:28