How to handle calendar TimeZones using Java?

JavaCalendarTimezone

Java Problem Overview


I have a Timestamp value that comes from my application. The user can be in any given local TimeZone.

Since this date is used for a WebService that assumes the time given is always in GMT, I have a need to convert the user's parameter from say (EST) to (GMT). Here's the kicker: The user is oblivious to his TZ. He enters the creation date that he wants to send to the WS, so what I need is:

User enters: 5/1/2008 6:12 PM (EST)
The parameter to the WS needs to be: 5/1/2008 6:12 PM (GMT)

I know TimeStamps are always supposed to be in GMT by default, but when sending the parameter, even though I created my Calendar from the TS (which is supposed to be in GMT), the hours are always off unless the user is in GMT. What am I missing?

Timestamp issuedDate = (Timestamp) getACPValue(inputs_, "issuedDate");
Calendar issueDate = convertTimestampToJavaCalendar(issuedDate);
...
private static java.util.Calendar convertTimestampToJavaCalendar(Timestamp ts_) {
  java.util.Calendar cal = java.util.Calendar.getInstance(
      GMT_TIMEZONE, EN_US_LOCALE);
  cal.setTimeInMillis(ts_.getTime());
  return cal;
}

With the previous Code, this is what I get as a result (Short Format for easy reading):

[May 1, 2008 11:12 PM]

Java Solutions


Solution 1 - Java

public static Calendar convertToGmt(Calendar cal) {

	Date date = cal.getTime();
	TimeZone tz = cal.getTimeZone();

	log.debug("input calendar has date [" + date + "]");

	//Returns the number of milliseconds since January 1, 1970, 00:00:00 GMT 
	long msFromEpochGmt = date.getTime();

	//gives you the current offset in ms from GMT at the current date
	int offsetFromUTC = tz.getOffset(msFromEpochGmt);
	log.debug("offset is " + offsetFromUTC);

	//create a new calendar in GMT timezone, set to this date and add the offset
	Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
	gmtCal.setTime(date);
	gmtCal.add(Calendar.MILLISECOND, offsetFromUTC);

	log.debug("Created GMT cal with date [" + gmtCal.getTime() + "]");

	return gmtCal;
}

Here's the output if I pass the current time ("12:09:05 EDT" from Calendar.getInstance()) in:

>DEBUG - input calendar has date [Thu Oct 23 12:09:05 EDT 2008]
>DEBUG - offset is -14400000
>DEBUG - Created GMT cal with date [Thu Oct 23 08:09:05 EDT 2008]

12:09:05 GMT is 8:09:05 EDT.

The confusing part here is that Calendar.getTime() returns you a Date in your current timezone, and also that there is no method to modify the timezone of a calendar and have the underlying date rolled also. Depending on what type of parameter your web service takes, your may just want to have the WS deal in terms of milliseconds from epoch.

Solution 2 - Java

Thank you all for responding. After a further investigation I got to the right answer. As mentioned by Skip Head, the TimeStamped I was getting from my application was being adjusted to the user's TimeZone. So if the User entered 6:12 PM (EST) I would get 2:12 PM (GMT). What I needed was a way to undo the conversion so that the time entered by the user is the time I sent to the WebServer request. Here's how I accomplished this:

// Get TimeZone of user
TimeZone currentTimeZone = sc_.getTimeZone();
Calendar currentDt = new GregorianCalendar(currentTimeZone, EN_US_LOCALE);
// Get the Offset from GMT taking DST into account
int gmtOffset = currentTimeZone.getOffset(
    currentDt.get(Calendar.ERA), 
    currentDt.get(Calendar.YEAR), 
    currentDt.get(Calendar.MONTH), 
    currentDt.get(Calendar.DAY_OF_MONTH), 
    currentDt.get(Calendar.DAY_OF_WEEK), 
    currentDt.get(Calendar.MILLISECOND));
// convert to hours
gmtOffset = gmtOffset / (60*60*1000);
System.out.println("Current User's TimeZone: " + currentTimeZone.getID());
System.out.println("Current Offset from GMT (in hrs):" + gmtOffset);
// Get TS from User Input
Timestamp issuedDate = (Timestamp) getACPValue(inputs_, "issuedDate");
System.out.println("TS from ACP: " + issuedDate);
// Set TS into Calendar
Calendar issueDate = convertTimestampToJavaCalendar(issuedDate);
// Adjust for GMT (note the offset negation)
issueDate.add(Calendar.HOUR_OF_DAY, -gmtOffset);
System.out.println("Calendar Date converted from TS using GMT and US_EN Locale: "
    + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
    .format(issueDate.getTime()));

The code's output is: (User entered 5/1/2008 6:12PM (EST)

Current User's TimeZone: EST
Current Offset from GMT (in hrs):-4 (Normally -5, except is DST adjusted)
TS from ACP: 2008-05-01 14:12:00.0
Calendar Date converted from TS using GMT and US_EN Locale: 5/1/08 6:12 PM (GMT)

Solution 3 - Java

You say that the date is used in connection with web services, so I assume that is serialized into a string at some point.

If this is the case, you should take a look at the setTimeZone method of the DateFormat class. This dictates which time zone that will be used when printing the time stamp.

A simple example:

SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));

Calendar cal = Calendar.getInstance();
String timestamp = formatter.format(cal.getTime());

Solution 4 - Java

You can solve it with Joda Time:

Date utcDate = new Date(timezoneFrom.convertLocalToUTC(date.getTime(), false));
Date localDate = new Date(timezoneTo.convertUTCToLocal(utcDate.getTime()));

Java 8:

LocalDateTime localDateTime = LocalDateTime.parse("2007-12-03T10:15:30");
ZonedDateTime fromDateTime = localDateTime.atZone(
    ZoneId.of("America/Toronto"));
ZonedDateTime toDateTime = fromDateTime.withZoneSameInstant(
    ZoneId.of("Canada/Newfoundland"));

Solution 5 - Java

It looks like your TimeStamp is being set to the timezone of the originating system.

This is deprecated, but it should work:

cal.setTimeInMillis(ts_.getTime() - ts_.getTimezoneOffset());

The non-deprecated way is to use

Calendar.get(Calendar.ZONE_OFFSET) + Calendar.get(Calendar.DST_OFFSET)) / (60 * 1000)

but that would need to be done on the client side, since that system knows what timezone it is in.

Solution 6 - Java

Method for converting from one timeZone to other(probably it works :) ).

/**
 * Adapt calendar to client time zone.
 * @param calendar - adapting calendar
 * @param timeZone - client time zone
 * @return adapt calendar to client time zone
 */
public static Calendar convertCalendar(final Calendar calendar, final TimeZone timeZone) {
    Calendar ret = new GregorianCalendar(timeZone);
    ret.setTimeInMillis(calendar.getTimeInMillis() +
            timeZone.getOffset(calendar.getTimeInMillis()) -
            TimeZone.getDefault().getOffset(calendar.getTimeInMillis()));
    ret.getTime();
    return ret;
}

Solution 7 - Java

Date and Timestamp objects are timezone-oblivious: they represent a certain number of seconds since the epoch, without committing to a particular interpretation of that instant as hours and days. Timezones enter the picture only in GregorianCalendar (not directly needed for this task) and SimpleDateFormat, which need a timezone offset to convert between separate fields and Date (or long) values.

The OP's problem is right at the beginning of his processing: the user inputs hours, which are ambiguous, and they are interpreted in the local, non-GMT timezone; at this point the value is "6:12 EST", which can be easily printed as "11.12 GMT" or any other timezone but is never going to change to "6.12 GMT".

There is no way to make the SimpleDateFormat that parses "06:12" as "HH:MM" (defaulting to the local time zone) default to UTC instead; SimpleDateFormat is a bit too smart for its own good.

However, you can convince any SimpleDateFormat instance to use the right time zone if you put it explicitly in the input: just append a fixed string to the received (and adequately validated) "06:12" to parse "06:12 GMT" as "HH:MM z".

There is no need of explicit setting of GregorianCalendar fields or of retrieving and using timezone and daylight saving time offsets.

The real problem is segregating inputs that default to the local timezone, inputs that default to UTC, and inputs that really require an explicit timezone indication.

Solution 8 - Java

Something that has worked for me in the past was to determine the offset (in milliseconds) between the user's timezone and GMT. Once you have the offset, you can simply add/subtract (depending on which way the conversion is going) to get the appropriate time in either timezone. I would usually accomplish this by setting the milliseconds field of a Calendar object, but I'm sure you could easily apply it to a timestamp object. Here's the code I use to get the offset

int offset = TimeZone.getTimeZone(timezoneId).getRawOffset();

timezoneId is the id of the user's timezone (such as EST).

Solution 9 - Java

java.time

The modern approach uses the java.time classes that supplanted the troublesome legacy date-time classes bundled with the earliest versions of Java.

The java.sql.Timestamp class is one of those legacy classes. No longer needed. Instead use Instant or other java.time classes directly with your database using JDBC 4.2 and later.

The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).

Instant instant = myResultSet.getObject( … , Instant.class ) ; 

If you must interoperate with an existing Timestamp, convert immediately into java.time via the new conversion methods added to the old classes.

Instant instant = myTimestamp.toInstant() ;

To adjust into another time zone, specify the time zone as a ZoneId object. Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter pseudo-zones such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZoneId z = ZoneId.of( "America/Montreal" ) ;

Apply to the Instant to produce a ZonedDateTime object.

ZonedDateTime zdt = instant.atZone( z ) ;

To generate a string for display to the user, search Stack Overflow for DateTimeFormatter to find many discussions and examples.

Your Question is really about going the other direction, from user data-entry to the date-time objects. Generally best to break your data-entry into two parts, a date and a time-of-day.

LocalDate ld = LocalDate.parse( dateInput , DateTimeFormatter.ofPattern( "M/d/uuuu" , Locale.US ) ) ;
LocalTime lt = LocalTime.parse( timeInput , DateTimeFormatter.ofPattern( "H:m a" , Locale.US ) ) ;

Your Question is not clear. Do you want to interpret the date and the time entered by the user to be in UTC? Or in another time zone?

If you meant UTC, create a OffsetDateTime with an offset using the constant for UTC, ZoneOffset.UTC.

OffsetDateTime odt = OffsetDateTime.of( ld , lt , ZoneOffset.UTC ) ;

If you meant another time zone, combine along with a time zone object, a ZoneId. But which time zone? You might detect a default time zone. Or, if critical, you must confirm with the user to be certain of their intention.

ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;

To get a simpler object that is always in UTC by definition, extract an Instant.

Instant instant = odt.toInstant() ;

…or…

Instant instant = zdt.toInstant() ; 

Send to your database.

myPreparedStatement.setObject( … , instant ) ;

About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJorge ValoisView Question on Stackoverflow
Solution 1 - Javamatt bView Answer on Stackoverflow
Solution 2 - JavaJorge ValoisView Answer on Stackoverflow
Solution 3 - JavaHenrik Aasted SørensenView Answer on Stackoverflow
Solution 4 - JavaVitalii FedorenkoView Answer on Stackoverflow
Solution 5 - JavaSkip HeadView Answer on Stackoverflow
Solution 6 - JavaHelpaView Answer on Stackoverflow
Solution 7 - Javauser412090View Answer on Stackoverflow
Solution 8 - JavaAdamView Answer on Stackoverflow
Solution 9 - JavaBasil BourqueView Answer on Stackoverflow