Hot questions for Using Joda-Time in timezone offset

Top Java Programmings / Joda-Time / timezone offset

Question:

I am trying to parse datetime strings and create Joda DateTime objects.

My data comes from a legacy database that stores datetime strings without specifying the timezone/offset. Although the timezone/offset of the datetime strings is not stored it is a business rule of the legacy system that all datetimes are stored in Eastern Time. Unfortunately I do not have the authority to update the way in which the legacy DB stores datetime strings.

Thus, I parse the datetime strings using JODA's "US/Eastern" time zone.

This approach throws an illegalInstance exception when the dateTime string falls within the hour that "disappears" when daylight savings is switched on.

I've created the following example code to demonstrate this behaviour and to show my proposed workaround.

public class FooBar {
public static final DateTimeZone EST = DateTimeZone.forID("EST");
public static final DateTimeZone EASTERN = DateTimeZone.forID("US/Eastern");

public static final DateTimeFormatter EST_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS").withZone(EST);
public static final DateTimeFormatter EASTERN_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS").withZone(EASTERN);


public static void main(String[] args) {
    final String[] listOfDateTimeStrings = {"2014-03-09 02:00:00.000", "2014-03-08 02:00:00.000"}; 

    System.out.println(" *********** 1st attempt  *********** ");
    for (String dateTimeString: listOfDateTimeStrings){
        try{
            final DateTime dateTime = DateTime.parse(dateTimeString, EASTERN_FORMATTER);
            System.out.println(dateTime);       
        }
        catch(Exception e){
            System.out.println(e.getMessage());
        }
    }

    System.out.println(" *********** 2nd attempt  *********** ");
    for (String dateTimeString: listOfDateTimeStrings){
        try{
            final DateTime dateTime = DateTime.parse(dateTimeString, EST_FORMATTER);
            System.out.println(dateTime);       
        }
        catch(Exception e){
            System.out.println(e.getMessage());
        }
    }

    System.out.println(" *********** 3rd attempt  *********** ");
    for (String dateTimeString: listOfDateTimeStrings){
        try{
            DateTime dateTime = DateTime.parse(dateTimeString, EST_FORMATTER);
            dateTime = dateTime.withZone(EASTERN);
            System.out.println(dateTime);       
        }
        catch(Exception e){
            System.out.println(e.getMessage());
        }
    }       

}

}

Output produced:

 *********** 1st attempt  *********** 
Cannot parse "2014-03-09 02:00:00.000": Illegal instant due to time zone offset transition (America/New_York)
2014-03-08T02:00:00.000-05:00
 *********** 2nd attempt  *********** 
2014-03-09T02:00:00.000-05:00
2014-03-08T02:00:00.000-05:00
 *********** 3rd attempt  *********** 
2014-03-09T03:00:00.000-04:00
2014-03-08T02:00:00.000-05:00

In the "3rd attempt" I get the expected result: the first datetime has an offset of -04:00. as it falls within the first hour of DST for 2015. The second timestamp has an offset of -05:00 as it falls outside of DST.

Is is safe to do this:

DateTime dateTime = DateTime.parse(dateTimeString, A_FORMATTER_WITH_TIME_ZONE_A);
dateTime = dateTime.withZone(TIME_ZONE_B);

I've tested this code with a few different combinations of datetime strings and time zones (and so far it works for all the test cases), but I was wondering if anyone with more Joda experience can see anything wrong/dangerous in this approach.

Or alternatively: is there a better way of doing handling the time zone offset transition with Joda?


Answer:

Be careful. The behaviour of the method withZone(...) is documented as follows:

Returns a copy of this datetime with a different time zone, preserving the millisecond instant.

Keeping this in mind, you have to understand that EST and "America/New_York" (better than the outdated id "US/Eastern") are not the same. First one (EST) has fixed offset (no DST), but second one has DST including possible gaps. You should only apply EST as replacement for Eastern if you are sure that

a) you are already in the exception mode (normally parsed datetimes in Eastern zone should be accepted without repeated parsing otherwise applying EST would falsify the parsed instant),

b) you understand choosing EST in second (and third) try is choosing the instant after DST transition.

Regarding this limitations/constraints, your workaround will work (but only for the special pair EST versus America/New_York). Personally I find it frightening to use exception-based logic to workaround a severe limitation of Joda-Time. As counter example, the new JSR-310 does not use the exception strategy when handling gaps but the strategy to choose the later instant after gap pushed forward by the size of the gap (like the old java.util.Calendar-stuff).

I recommend you to first follow the advise of @Jim Garrison to look if the crapped data can be corrected before you apply such a workaround (my upvote for his answer).

Update after reading your original spec requirement (obsolete - see below):

If the spec of the legacy system says that all times are stored in EST, then you should parse it this way and NOT use "America/New_York" for parsing at all. Instead you can convert the parsed EST-instants to New-York-time in second phase (using withZone(EASTERN). This way you will not have any exception logic because a (parsed) instant can always be converted to a local time representation in an unambigous way (the parsed instant of type DateTime, the converted result contains a local time). Code example:

public static final DateTimeZone EST = DateTimeZone.forID("EST");
public static final DateTimeZone EASTERN = DateTimeZone.forID("US/Eastern");
public static final DateTimeFormatter EST_FORMATTER = 
  DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS").withZone(EST);

// in your parsing method...
String input = "2014-03-09 02:00:00.000";
DateTime dt = EST_FORMATTER.parseDateTime(input);
System.out.println(dt); // 2014-03-09T02:00:00.000-05:00
System.out.println(dt.withZone(EASTERN)); // 2014-03-09T03:00:00.000-04:00

Update after comment and clarification of OP:

Now it is confirmed that the legacy system does not store timestamps in EST (with fixed offset UTC-05 but in EASTERN zone ("America/New_York" with variable offset of either EST or EDT). First action should be to contact the supplier of invalid timestamps in order to see if they can correct the data. Else you can use following workaround:

Regarding the fact that your input contains timestamps without any offset or zone information I recommend first to parse as LocalDateTime.

=> static initialization part

// Joda-Time cannot parse "EDT" so we use hard-coded offsets
public static final DateTimeZone EST = DateTimeZone.forOffsetHours(-5);
public static final DateTimeZone EDT = DateTimeZone.forOffsetHours(-4);

public static final DateTimeZone EASTERN = DateTimeZone.forID("America/New_York");
public static final org.joda.time.format.DateTimeFormatter FORMATTER = 
    org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS");

=> in your parse method

String input = "2014-03-09 02:00:00.000";
LocalDateTime ldt = FORMATTER.parseLocalDateTime(input); // always working
System.out.println(ldt); // 2014-03-09T02:00:00.000
DateTime result;

try {
    result = ldt.toDateTime(EASTERN);
} catch (IllegalInstantException ex) {
    result = ldt.plusHours(1).toDateTime(EDT); // simulates a PUSH-FORWARD-strategy at gap
    // result = ldt.toDateTime(EST); // the same instant but finally display with EST offset
}
System.out.println(result); // 2014-03-09T03:00:00.000-04:00
// if you had chosen <<<ldt.toDateTime(EST)>>> then: 2014-03-09T02:00:00.000-05:00

Another clarification due to last comment of OP:

The method toDateTime(DateTimeZone) producing a DateTime is documented as follows:

In a daylight saving overlap, when the same local time occurs twice, this method returns the first occurrence of the local time.

In other words, it chooses the earlier offset in case of overlap (in autumn). So there is no need to call

result = ldt.toDateTime(EASTERN).withEarlierOffsetAtOverlap();

However, it does not do any harm here, and you might prefer it for sake of documentation. On the other side: It does not make any sense to call in the exception handling (for gaps)

result = ldt.toDateTime(EDT).withEarlierOffsetAtOverlap();

because EDT (and EST, too) is a fixed offset where overlaps can never occur. So here the method withEarlierOffsetAtOverlap() does not do anything. Furthermore: Leaving out the correction ldt.plusHours(1) in case of EDT is not okay and will yield another instant. Already tested by me before writing this extra explanation, but of course, you can use the alternative ldt.toDateTime(EST) to achieve what you want (EDT != EST, but with the correction of plusHours(1) you get same instants). I have just noticed the EDT example to demonstrate how you can exactly model the standard JDK-behaviour. It is up to you which offset you prefer in case of resolving gaps (EDT or EST) but getting the same instants is vital here (ldt.plusHours(1).toDateTime(EDT) versus result = ldt.toDateTime(EST)).

Question:

Given a timezone (America/New_York), how do I go about getting the UTC offset?

I have tried using java.util.TimeZone but had no luck. I am fine with using Joda Time as well if the solution is viable in that.


Answer:

The offset for a particular time zone can vary based on the current time because of daylight savings, etc. UTC doesn't have daylight savings, but "America/New_York" will change offsets with the daylight savings switch. Therefore, the offset is a function of both the current time and the timezone. The answers here give some examples of how to get the current offset:

Java TimeZone offset

Question:

I am using jodatime now, but I got one issue recently, which is: when I get a joda time (it always with timezone offset), say 2017-01-31T00:00:00.000+08:00, after I insert this value to database (I am using Hibernate4 as my ORM framework), the value will automatically convert to UTC time, which is 1/30/2017 4:00:00 PM (I am using IBM DB2, tried MySql also same). And when I retrieve it from database, it will convert back to 2017-01-31T00:00:00.000+08:00.

Just wondering why this happens? Can I do some global configuration on this? I want to store local time instead of UTC time in database (something like 1/31/2017 0:00:00 AM), so I need to change DB setting or do it at code level? Really no much experience on this timezone handling, can anyone help me? Thanks!


Answer:

After few hours' investigation, I finally found the solution for this issue, since I am using Hibernate, what I need to do is just add below line to hibernate config xml:

<property name="jadira.usertype.databaseZone">Asia/Singapore</property>
<property name="jadira.usertype.javaZone">Asia/Singapore</property>

alternatively:

<property name="jadira.usertype.databaseZone">jvm</property>
<property name="jadira.usertype.javaZone">jvm</property>

Now everything works fine.

Question:

I would like to convert:

2014-08-12T05:43:00-05:00 (YYYY-MM-DD"T"HH:MM:SS-OFFSET)

To:

20140812104300Z (YYYYMMDDHHMMSSZ)

Answer:

Finally got it:

import java.text.ParseException;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

public class mydate {

    public static void main(String[] args) throws ParseException {
        // TODO Auto-generated method stub

        java.util.Date date = new DateTime("2014-08-12T05:43:00-05:00").toDate();
        DateTime dateTimeUtc = new DateTime( date, DateTimeZone.UTC ); // Joda-Time can convert from java.util.Date type which has no time zone.
        String output = dateTimeUtc.toString().replace("-", "").replace("T", "").replace(":", "").substring(0,14)+"Z"; // Defaults to ISO 8601 format.
        System.out.println(output);

    }

}

Input: 2014-08-12T05:43:00-05:00

Output: 20140812104300Z