Skip to content
Advertisement

Java Calendar clear() changes DST

First, I want to state that I know the Java Calendar class is being supplanted by other libraries that are arguably better. Perhaps I’ve stumbled upon one of the reasons Calendar has fallen out of favor.

I ran into frustrating behavior in Calendar as it regards to the overlapping hour at the end of daylight savings time.

public void annoying_issue()
{
    Calendar midnightPDT = Calendar.getInstance(TimeZone.getTimeZone("US/Pacific"));
    midnightPDT.set(Calendar.YEAR, 2021);
    midnightPDT.set(Calendar.MONTH, 10);
    midnightPDT.set(Calendar.DAY_OF_MONTH, 7);
    midnightPDT.set(Calendar.HOUR_OF_DAY, 0);
    midnightPDT.set(Calendar.MINUTE, 0);
    midnightPDT.set(Calendar.SECOND, 0);
    midnightPDT.set(Calendar.MILLISECOND, 0);

    Calendar oneAMPDT = Calendar.getInstance(TimeZone.getTimeZone("US/Pacific"));
    oneAMPDT.setTimeInMillis(midnightPDT.getTimeInMillis() + (60*60*1000));//this is the easiest way I've found to get to the first 1am hour at DST overlap

    System.out.println(new Date(midnightPDT.getTimeInMillis()));//prints the expected "Sun Nov 7 00:00:00 PDT 2021" 
    System.out.println(new Date(oneAMPDT.getTimeInMillis()));//prints "Sun Nov 7 01:00:00 PDT 2021" also expected

    oneAMPDT.clear(Calendar.MINUTE);//minute is already 0 so no change should occur... RIGHT!? 
    
    //WRONG!!!!
    //The time is now in PST! The millisecond value has increased by 3600000, too!!
    System.out.println(new Date(oneAMPDT.getTimeInMillis()));//prints "Sun Nov 7 01:00:00 PST 2021"
}

Following along with the comments you’ll see that clearing the MINUTE field in the calendar actually moved it up an hour! The HECK!?

This also occurs when I use oneAMPDT.set(Calendar.MINUTE, 0)

Is this expected behavior? Is there a way to prevent this?

Advertisement

Answer

Avoid legacy date-time classes; convert if needed

As you noted, Calendar was supplanted years ago by the java.time classes defined in JSR 310 (unanimously adopted). And as you note there are many reasons to avoid using Calendar & Date etc.

If you must have a Calendar object to interoperate with old code not yet updated to java.time, convert after doing your work in java.time.

java.time

Specify your desired time zone. Note that US/Pacific is merely an alias for the actual time zone, America/Los_Angeles.

ZoneId zLosAngeles = ZoneId.of( "America/Los_Angeles" ) ;

Specify your desired moment.

LocalDate ld = LocalDate.of( 2021 , Month.NOVEMBER , 7 ) ;

In your code, you seem to assume the first moment of the day occurs at 00:00. That is not always the case. Some dates in some time zones may start at another time. So let java.time determine the first moment of the day.

ZonedDateTime firstMomentOfThe7thInLosAngeles = ld.atStartOfDay( zLosAngeles ) ;

firstMomentOfThe7thInLosAngeles.toString(): 2021-11-07T00:00-07:00[America/Los_Angeles]

But then you jumped to another moment, to 1 AM.

ZonedDateTime oneAmOnThe7thLosAngeles = firstMomentOfThe7thInLosAngeles.with( LocalTime.of( 1 , 0 ) ) ;

oneAmOnThe7thLosAngeles.toString(): 2021-11-07T01:00-07:00[America/Los_Angeles]

That time-of-day may or may not exist on that date in that zone. The ZonedDateTime class will adjust if need be.

You used the name midnightPDT for a variable. I suggest avoiding the term midnight as its use confuses date-time handling with out a precise definition. I recommend using the term “first moment of the day” if that is what you mean.

You extract a count of milliseconds since the epoch reference of first moment of 1970 as seen in UTC, 1970-01-01T00:00Z.

Instant firstMomentOfThe7thInLosAngelesAsSeenInUtc = firstMomentOfThe7thInLosAngeles.toInstant() ;
long millisSinceEpoch_FirstMomentOf7thLosAngeles = firstMomentOfThe7thInLosAngelesAsSeenInUtc.toEpochMilli() ;

firstMomentOfThe7thInLosAngelesAsSeenInUtc.toString(): 2021-11-07T07:00:00Z

millisSinceEpoch_FirstMomentOf7thLosAngeles = 1636268400000

And you do the same for our 1 AM moment.

Instant oneAmOnThe7thLosAngelesAsSeenInUtc = oneAmOnThe7thLosAngeles.toInstant() ;
long millisSinceEpoch_OneAmOn7thLosAngeles = oneAmOnThe7thLosAngelesAsSeenInUtc.toEpochMilli() ;

oneAmOnThe7thLosAngelesAsSeenInUtc.toString(): 2021-11-07T08:00:00Z

millisSinceEpoch_OneAmOn7thLosAngeles = 1636272000000

We should see a difference of one hour. An hour = 3,600,000 = 60 * 60 * 1,000.

long diff = ( millisSinceEpoch_OneAmOn7thLosAngeles - millisSinceEpoch_FirstMomentOf7thLosAngeles );  // 3,600,000 = 60 * 60 * 1,000.

diff = 3600000

Cutover

Then you go on to mention the Daylight Saving Time (DST) cutover. The cutover for DST in the United States on that date was 2 AM, not 1 AM. At the moment of 2 AM arriving, the clocks swung back to 1 AM, for a second 1:00-2:00 AM hour.

To get to that point of cutover, let’s add an hour.

ZonedDateTime cutover_Addition = oneAmOnThe7thLosAngeles.plusHours( 1 );

cutover_Addition = 2021-11-07T01:00-08:00[America/Los_Angeles]

Notice that the time-of-day shows the same (1 AM), but the offset-from-UTC has changed from being 7 hours behind UTC to now 8 hours behind UTC. There lies the hour difference you seek.

Let’s get the count of milliseconds since epoch for this third moment. Before we had first moment of the day (00:00), then the first occurring 1 AM, and now we have the second occurring 1 AM on this “Fall-Back” date of November 7, 2021.

long millisSinceEpoch_Cutover = cutover_Addition.toInstant().toEpochMilli();

1636275600000

Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT2H

Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT1H

The ZonedDateTime class does offer a pair of methods of use at these moments of cutover: withEarlierOffsetAtOverlap and withLaterOffsetAtOverlap.

ZonedDateTime cutover_OverlapEarlier =
        cutover_Addition
                .withEarlierOffsetAtOverlap();
ZonedDateTime cutover_OverlapLater =
        cutover_Addition
                .withLaterOffsetAtOverlap();

cutover_OverlapEarlier = 2021-11-07T01:00-07:00[America/Los_Angeles]

cutover_OverlapLater = 2021-11-07T01:00-08:00[America/Los_Angeles]

Calendar

If you really need a Calendar object, just convert.

Calendar x = GregorianCalendar.from( firstMomentOfThe7thInLosAngeles ) ;
Calendar y = GregorianCalendar.from( oneAmOnThe7thLosAngeles ) ;
Calendar z = GregorianCalendar.from( cutover_Addition );

If you goal is simply struggling with understanding Calendar class behavior, I suggest you stop the masochism. There is no point. Sun, Oracle, and the JCP community all gave up on those terrible legacy date-time classes. I suggest you do the same.

Example code

Pulling together all that code above.

ZoneId zLosAngeles = ZoneId.of( "America/Los_Angeles" );

LocalDate ld = LocalDate.of( 2021 , Month.NOVEMBER , 7 );

ZonedDateTime firstMomentOfThe7thInLosAngeles = ld.atStartOfDay( zLosAngeles );
ZonedDateTime oneAmOnThe7thLosAngeles = firstMomentOfThe7thInLosAngeles.with( LocalTime.of( 1 , 0 ) );

Instant firstMomentOfThe7thInLosAngelesAsSeenInUtc = firstMomentOfThe7thInLosAngeles.toInstant();
long millisSinceEpoch_FirstMomentOf7thLosAngeles = firstMomentOfThe7thInLosAngelesAsSeenInUtc.toEpochMilli();

Instant oneAmOnThe7thLosAngelesAsSeenInUtc = oneAmOnThe7thLosAngeles.toInstant();
long millisSinceEpoch_OneAmOn7thLosAngeles = oneAmOnThe7thLosAngelesAsSeenInUtc.toEpochMilli();

long diff = ( millisSinceEpoch_OneAmOn7thLosAngeles - millisSinceEpoch_FirstMomentOf7thLosAngeles );  // 3,600,000 = 60 * 60 * 1,000.

ZonedDateTime cutover_Addition = oneAmOnThe7thLosAngeles.plusHours( 1 );
long millisSinceEpoch_Cutover = cutover_Addition.toInstant().toEpochMilli();
ZonedDateTime cutover_OverlapEarlier =
        cutover_Addition
                .withEarlierOffsetAtOverlap();
ZonedDateTime cutover_OverlapLater =
        cutover_Addition
                .withLaterOffsetAtOverlap();

Convert to legacy classes, if need be.

Calendar x = GregorianCalendar.from( firstMomentOfThe7thInLosAngeles );
Calendar y = GregorianCalendar.from( oneAmOnThe7thLosAngeles );
Calendar z = GregorianCalendar.from( cutover_Addition );

Dump to console.

System.out.println( "firstMomentOfThe7thInLosAngeles = " + firstMomentOfThe7thInLosAngeles );
System.out.println( "oneAmOnThe7thLosAngeles = " + oneAmOnThe7thLosAngeles );

System.out.println( "firstMomentOfThe7thInLosAngelesAsSeenInUtc = " + firstMomentOfThe7thInLosAngelesAsSeenInUtc );
System.out.println( "millisSinceEpoch_FirstMomentOf7thLosAngeles = " + millisSinceEpoch_FirstMomentOf7thLosAngeles );

System.out.println( "oneAmOnThe7thLosAngelesAsSeenInUtc = " + oneAmOnThe7thLosAngelesAsSeenInUtc );
System.out.println( "millisSinceEpoch_OneAmOn7thLosAngeles = " + millisSinceEpoch_OneAmOn7thLosAngeles );

System.out.println( "diff = " + diff );

System.out.println( "x = " + x );
System.out.println( "y = " + y );
System.out.println( "z = " + z );

System.out.println( "cutover_Addition = " + cutover_Addition );
System.out.println( "millisSinceEpoch_Cutover = " + millisSinceEpoch_Cutover );
System.out.println( "Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = " + Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) );
System.out.println( "Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = " + Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) );
System.out.println( "cutover_OverlapEarlier = " + cutover_OverlapEarlier );
System.out.println( "cutover_OverlapLater = " + cutover_OverlapLater );

When run.

firstMomentOfThe7thInLosAngeles = 2021-11-07T00:00-07:00[America/Los_Angeles]
oneAmOnThe7thLosAngeles = 2021-11-07T01:00-07:00[America/Los_Angeles]
firstMomentOfThe7thInLosAngelesAsSeenInUtc = 2021-11-07T07:00:00Z
millisSinceEpoch_FirstMomentOf7thLosAngeles = 1636268400000
oneAmOnThe7thLosAngelesAsSeenInUtc = 2021-11-07T08:00:00Z
millisSinceEpoch_OneAmOn7thLosAngeles = 1636272000000
diff = 3600000
x = java.util.GregorianCalendar[time=1636268400000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2021,MONTH=10,WEEK_OF_YEAR=44,WEEK_OF_MONTH=1,DAY_OF_MONTH=7,DAY_OF_YEAR=311,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]
y = java.util.GregorianCalendar[time=1636272000000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2021,MONTH=10,WEEK_OF_YEAR=44,WEEK_OF_MONTH=1,DAY_OF_MONTH=7,DAY_OF_YEAR=311,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=1,HOUR_OF_DAY=1,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]
z = java.util.GregorianCalendar[time=1636275600000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2021,MONTH=10,WEEK_OF_YEAR=44,WEEK_OF_MONTH=1,DAY_OF_MONTH=7,DAY_OF_YEAR=311,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=1,HOUR_OF_DAY=1,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=0]
cutover_Addition = 2021-11-07T01:00-08:00[America/Los_Angeles]
millisSinceEpoch_Cutover = 1636275600000
Duration.between( firstMomentOfThe7thInLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT2H
Duration.between( oneAmOnThe7thLosAngelesAsSeenInUtc , cutover_Addition.toInstant() ) = PT1H
cutover_OverlapEarlier = 2021-11-07T01:00-07:00[America/Los_Angeles]
cutover_OverlapLater = 2021-11-07T01:00-08:00[America/Los_Angeles]
Advertisement