Question

I am trying to change the timezone of a calendar object while maintaining the actual date/time so I can call getTimeInMilliseconds() on the updated calendar object.

I looked at this other question. Here, the accepted answer is to individually set each field of a new Calendar object. However, I was able to make it work by simply changing the timezone of a copy of the original Calendar object. Strange thing is, this only works if I make some modification to the original Calendar before resetting the timeZone. The following example illustrates the inconsistency.

public void TestCalendar()
{
   Calendar nextYear = Calendar.getInstance();
   nextYear.add(Calendar.YEAR, 1);
   log.info("Next Year: {}", getUTCMilliseconds(nextYear));

   Calendar now = Calendar.getInstance();
   log.info("Now: {}", getUTCMilliseconds(now));
}

protected String getUTCMilliseconds(Calendar cal)
{
   // Create a new calendar so we don't modify input
   Calendar expectedDbTime = (Calendar) cal.clone();
   // Change the TimeZone the contained date is interpreted in
   expectedDbTime.setTimeZone(TimeZone.getTimeZone("UTC"));
   // Return millisecond value of this date in the UTC timezone
   return String.valueOf(expectedDbTime.getTimeInMillis());
}

I ran this program on Jan 24th, 2015 @ 2:33 PM, and got the following output:

Next Year: 1422110038529 //(Corresponding UTC Date: Sat Jan 24 2015 2:33:58 PM)
Now: 1390599238531 //(Corresponding UTC Date: Fri Jan 24 2014 9:33:58 PM)

As you can see, the nextYear prints as expected, however the next line is not as expected (It should correspond to 1/24/2014 2:33:58PM UTC, instead, it corresponds to current date/time which is 1/24/2014 2:33:58 MTN). Can someone tell me what's going on here?

EDIT: Just updated some formatting.

Was it helpful?

Solution

Setting a timezone does not work like setting a field. When you set a field, the internal # of milliseconds is re-calculated to match the fields. When you set a timezone, the fields are recalculated so that the # of milliseconds remains constant! (That is, when you have a clanedar evaluated to 2:00 PM mountain and you set it to UTC, it does not calculate the milliseconds that represent 2PM UTC, it changes the time to 9PM.)

Now here's where it goes sideways, when you have a pending set (or add in your case) the flag that tells Calendar to recalculate the number of milliseconds (turned on by add) takes precedence over the flag that tells if to recalculate the text fields off the milliseconds (turned on by setTimeZone)

So scenario 1 Calendar has text value:

Sat Jan 24 2015 2:33:58 PM UTC and because add was called the flag that says "recompute milliseconds based on 'human' values" is turned on, and it gives you the # you expected.

Scenario 2 Calendar has text value:

Sat Jan 24 2015 2:33:58 PM UTC, but the flag that says to recompute # of milliseconds is not on, so instead the flag turned on by set timezone to "recompute 'human values' based on milliseconds" is turned on and it changes it to 9pm.

If you change your code to:

public void TestCalendar() {
    Calendar nextYear = Calendar.getInstance();
    nextYear.add(Calendar.YEAR, 1);
    nextYear.getTime();
    log.info("Next Year: {}", getUTCMilliseconds(nextYear));

    Calendar now = Calendar.getInstance();
    log.info("Now: {}", getUTCMilliseconds(now));
}

You will see consistent behaviour between the two because calling getTime() clears the flag to recalculate milliseconds prior to setting the timezone.

Now you know why the stock answer to a question like this is 'omg use jodatime'!

The 'safe' way to get what you were expecting to happen is:

protected String getUTCMilliseconds(Calendar cal) {
    Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
    utcCal.set(Calendar.YEAR, cal.get(Calendar.YEAR));
    utcCal.set(Calendar.DAY_OF_YEAR, cal.get(Calendar.DAY_OF_YEAR)); 
    utcCal.set(Calendar.HOUR_OF_DAY, cal.get(Calendar.HOUR_OF_DAY));
    utcCal.set(Calendar.MINUTE, cal.get(Calendar.MINUTE));
    utcCal.set(Calendar.SECOND, cal.get(Calendar.SECOND));
    utcCal.set(Calendar.MILLISECOND, cal.get(Calendar.MILLISECOND));
    return String.valueOf(utcCal.getTimeInMillis());
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top