Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GregorianCalendar DST problems

If I take the time at the end of the autumn day light time shift (2014-10-26 02:00:00 CET in Denmark) and subtract one hour (so I would expect to go back to 02:00 CEST) and then set the minutes to zero, I get some strange results:

Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("CET"));

cal.set(Calendar.YEAR, 2014);
cal.set(Calendar.MONTH, Calendar.OCTOBER);
cal.set(Calendar.DAY_OF_MONTH, 26);
cal.set(Calendar.HOUR_OF_DAY, 2);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);

System.out.println(cal.getTimeInMillis()); // 1414285200000 : 01:00:00 UTC

cal.add(Calendar.HOUR_OF_DAY, -1);

System.out.println(cal.getTimeInMillis()); // 1414281600000 : 00:00:00 UTC (as expected)

cal.set(Calendar.MINUTE, 0);
// or: cal.set(Calendar.MINUTE, cal.get(Calendar.MINUTE));
// both should be NOPs.

System.out.println(cal.getTimeInMillis()); // 1414285200000 : 01:00:00 UTC (Why?!)

Is this a bug? I know that Java Calendar has some strange conventions, but I cannot see how this could be correct.

What is the correct way to subtract an hour and set the minutes to 0 using the Calendar class?

like image 242
Rasmus Faber Avatar asked Mar 20 '14 08:03

Rasmus Faber


3 Answers

Here are the comments to the similar "bug" from Java Bug tracker:

During the "fall-back" period, Calendar doesn't support disambiguation and the given local time is interpreted as standard time.

To avoid the unexpected DST to standard time change, call add() to reset the value.

This means you should replace set() with

cal.add(Calendar.MINUTE, -cal.get(Calendar.MINUTE));
like image 85
apangin Avatar answered Nov 19 '22 16:11

apangin


The link in apangin's response explains the problem and suggest a solution. I did tried to look at it the hard way looking throught the code.

If we check DST_OFFSET before and after our set:

System.out.println(cal.get(Calendar.DST_OFFSET));
cal.set(Calendar.MINUTE, 0);
System.out.println(cal.get(Calendar.DST_OFFSET));

It prints:

3600000
0

Looking at the methodgetTimeInMillis we see they use a flag (isTimeSet) to check when recalculating the time in millis:

public long getTimeInMillis() {
   if (!isTimeSet) {
       updateTime();
   }
   return time;
}

The flag isTimeSet is reset to false when calling set. From the source of GregorianCalendar:

public void set(int field, int value)
{
    if (isLenient() && areFieldsSet && !areAllFieldsSet) {
        computeFields();
    }
    internalSet(field, value);
    isTimeSet = false;             // <-------------- here
    areFieldsSet = false;
    isSet[field] = true;
    stamp[field] = nextStamp++;
    if (nextStamp == Integer.MAX_VALUE) {
        adjustStamp();
    }
}

So set reset the DST offset. This line gets the offsets:

zoneOffset = ((ZoneInfo)tz).getOffsets(time, zoneOffsets);

A little down the values get set:

internalSet(DST_OFFSET, zoneOffsets[1]); 

And the time in millis is changed onnext call to getTimeInMillis.

As apangin suggest we should not use set after inititializing the date or we would force a sort of date reset. This is how is suggested in the post in the bug tracker of OpenJDK.

cal.add(Calendar.MINUTE, -cal.get(Calendar.MINUTE));

Better alternatives is using Joda Time. The author of this library has been working on new Java 8's date time api, I hope doesnt have this kind of issues.

like image 31
Raul Guiu Avatar answered Nov 19 '22 15:11

Raul Guiu


Calendar.add method must act as follows:

Adds or subtracts the specified amount of time to the given calendar field, based on the calendar's rules.

So we faced here with mysterious calendar's rules. What is the calendar type we've got by Calendar.getInstance(TimeZone.getTimeZone("CET"))?

Execute

System.out.println(Calendar.getInstance(TimeZone.getTimeZone("CET")).getClass());

I've got answer: class java.util.GregorianCalendar. Let's try to find rules of GregorianCalendar. I found them in add method (why here?). One of rules fits your case.

Add rule 2. If a smaller field is expected to be invariant, but it is impossible for it to be equal to its prior value because of changes in its minimum or maximum after field is changed, then its value is adjusted to be as close as possible to its expected value

01:00:00 and 23:00:00 have the same distance, so both are valid by these rules.

Also please note next line in GregorianCalendar.add specification

Adds the specified (signed) amount of time

It says you were using it wrong calling cal.add(Calendar.HOUR_OF_DAY, -1); Hence you have no one to blame that your add shows result only on next set invocation. But it somehow works; you can use it!

What's to do here? If you're OK that 2014-10-26 02:00:00 CET minus one hour equals itself - then you can just add stupid set after your add. Like that:

    cal.add(Calendar.HOUR_OF_DAY, -1);
    cal.set(Calendar.HOUR, cal.get(Calendar.HOUR)); //apply changes
    cal.set(Calendar.MINUTE, 0);

If you want to see the difference after -1 hour operation then I would advice you to change TimeZone. TimeZone.getTimeZone("GMT") (as example) has no daylight saving time offset.

like image 1
Sergey Fedorov Avatar answered Nov 19 '22 14:11

Sergey Fedorov