GregorianCalendar DST problems
Asked Answered
W

3

15

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?

Welcome answered 20/3, 2014 at 8:55 Comment(3)
Man it's weird ! just tried it and got exact same thing ! it's like it remembers it first value after the setDever
required comment about joda time here while I boot up my ide to check thisBadajoz
I think it's rolling the Hour value forward (or pulling the default set state of the Calendar?. Reversing the commands (setting the minute, then adding the negative hour) seems to work, but it's a bit of a workaround for such an obvious problem.Publicly
A
11

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));
Atomism answered 24/3, 2014 at 15:9 Comment(1)
One more explanation of the similar problem.Atomism
W
5

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.

Walcott answered 24/3, 2014 at 15:8 Comment(0)
J
1

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.

Joule answered 24/3, 2014 at 14:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.