Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Joda time does time conversion 'too soon'

We have an application where the timing is critical. We're using joda to do time conversions and storing all data in UTC time. We've been in production for a while and everthing has been be perfect BUT...

Now we notice that events occuring a few hours before the time change are already converted too early! In fact, the UTC times saved to the database are off by an hour.

Here is an example. My event occurs at on 11/6/2010 @ 9pm PDT and would normally be saved as 11/7/2010 @ 4am. However, because Daylight Savings Time ended on the 7th (presumably at 2am), this time gets shifted and stored as 11/7/2010 @ 5am.

We need the DST change to not be recorded until it actually occurs in the PST region, at 2am PST. I assumed joda would handle this, especially since it is touted to be much improved over java's default functionality.

Any feedback you have would be helpful, especially if you can get it to us before the time change tomorrow! After that it'll be academic, but still a useful discussion.

Here is some of the code we use to perform a timezone change and get the result as a regular java date object.

public Date convertToTimeZone(Date dt, TimeZone from, TimeZone to){
    DateTimeZone tzFrom = DateTimeZone.forTimeZone(from);
    DateTimeZone tzTo = DateTimeZone.forTimeZone(to);

    Date utc = new Date(tzFrom.convertLocalToUTC(dt.getTime(), false));
    Date convertedTime = new Date(tzTo.convertUTCToLocal(utc.getTime()));
    return convertedTime;
}

Edit: Code sample for comments below

public Date convert(Date dt, TimeZone from, TimeZone to) {
    long fromOffset = from.getOffset(dt.getTime());
    long toOffset = to.getOffset(dt.getTime());

    long convertedTime = dt.getTime() - (fromOffset - toOffset);
    return new Date(convertedTime);
}

Complete Unit Test Example

package com.test.time;

import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Instant;
import org.junit.Before;
import org.junit.Test;

public class TimeTest {
Calendar nov6;
Calendar nov1;
Calendar nov12;

@Before
public void doBefore() {
    // November 1st 2010, 9:00pm (DST is active)
    nov1 = Calendar.getInstance();
    nov1.setTimeZone(TimeZone.getTimeZone("US/Arizona"));
    nov1.set(Calendar.HOUR_OF_DAY, 21);
    nov1.set(Calendar.MINUTE, 0);
    nov1.set(Calendar.SECOND, 0);
    nov1.set(Calendar.YEAR, 2010);
    nov1.set(Calendar.MONTH, 10); // November
    nov1.set(Calendar.DATE, 1);

    // November 6st 2010, 9:00pm (DST is still active until early AM november 7th)
    nov6 = Calendar.getInstance();
    nov6.setTimeZone(TimeZone.getTimeZone("US/Arizona"));
    nov6.set(Calendar.HOUR_OF_DAY, 21);
    nov6.set(Calendar.MINUTE, 0);
    nov6.set(Calendar.SECOND, 0);
    nov6.set(Calendar.YEAR, 2010);
    nov6.set(Calendar.MONTH, 10); // November
    nov6.set(Calendar.DATE, 6);

    // November 12th 2010, 9:00pm (DST has ended)
    nov12 = Calendar.getInstance();
    nov12.setTimeZone(TimeZone.getTimeZone("US/Arizona"));
    nov12.set(Calendar.HOUR_OF_DAY, 21);
    nov12.set(Calendar.MINUTE, 0);
    nov12.set(Calendar.SECOND, 0);
    nov12.set(Calendar.YEAR, 2010);
    nov12.set(Calendar.MONTH, 10); // November
    nov12.set(Calendar.DATE, 12);
}

@Test
public void test1() {
    //      System.out.println("test1");
    timeTestJava(nov1.getTime(), "equivalent", "US/Arizona", "US/Pacific");
    timeTestJodaOld(nov1.getTime(), "equivalent", "US/Arizona", "US/Pacific");
    timeTestJodaNew(nov1.getTime(), "equivalent", "US/Arizona", "US/Pacific");

    timeTestJava(nov6.getTime(), "equivalent", "US/Arizona", "US/Pacific");
    timeTestJodaOld(nov6.getTime(), "equivalent", "US/Arizona", "US/Pacific");
    timeTestJodaNew(nov6.getTime(), "equivalent", "US/Arizona", "US/Pacific");

    timeTestJava(nov12.getTime(), "minus1", "US/Arizona", "US/Pacific");
    timeTestJodaOld(nov12.getTime(), "minus1", "US/Arizona", "US/Pacific");
    timeTestJodaNew(nov12.getTime(), "minus1", "US/Arizona", "US/Pacific");
}

private void timeTestJodaOld(Date startTime, String text, String from, String to) {
    System.out.println("joda_old: " + startTime);
    Date output = convertJodaOld(startTime, TimeZone.getTimeZone(from), TimeZone.getTimeZone(to));
    System.out.println(text + ": " + output + "\n");
}

private void timeTestJodaNew(Date startTime, String text, String from, String to) {
    System.out.println("joda_new: " + startTime);
    Date output = convertJodaNew(startTime, TimeZone.getTimeZone(from), TimeZone.getTimeZone(to));
    System.out.println(text + ": " + output + "\n");
}

private void timeTestJava(Date startTime, String text, String from, String to) {
    System.out.println("java: " + startTime);
    Date output = convertJava(startTime, TimeZone.getTimeZone(from), TimeZone.getTimeZone(to));
    System.out.println(text + ": " + output + "\n");
}

// Initial Joda implementation, works before and after DST change, but not during the period from 2am-7am UTC on the day of the change
public Date convertJodaOld(Date dt, TimeZone from, TimeZone to) {
    DateTimeZone tzFrom = DateTimeZone.forTimeZone(from);
    DateTimeZone tzTo = DateTimeZone.forTimeZone(to);

    Date utc = new Date(tzFrom.convertLocalToUTC(dt.getTime(), false));
    Date convertedTime = new Date(tzTo.convertUTCToLocal(utc.getTime()));
    return convertedTime;
}

// New attempt at joda implementation, doesn't work after DST (winter)
public Date convertJodaNew(Date dt, TimeZone from, TimeZone to) {
    Instant utcInstant = new Instant(dt.getTime());
    DateTime datetime = new DateTime(utcInstant);

    datetime.withZone(DateTimeZone.forID(to.getID()));
    return datetime.toDate();
}

// Java implementation.  Works.
public Date convertJava(Date dt, TimeZone from, TimeZone to) {
    long fromOffset = from.getOffset(dt.getTime());
    long toOffset = to.getOffset(dt.getTime());

    long convertedTime = dt.getTime() - (fromOffset - toOffset);
    return new Date(convertedTime);
}

}

like image 256
samspot Avatar asked Nov 05 '10 21:11

samspot


People also ask

Is Joda DateTime deprecated?

So the short answer to your question is: YES (deprecated).

Does Joda DateTime have time zones?

Joda-Time uses immutable objects. So rather than change the time zone ("mutate"), we instantiate a new DateTime object based on the old but with the desired difference (some other time zone).

What is the replacement of Joda-Time Library in Java 8?

By tackling this problem head-on, Joda-Time became the de facto standard date and time library for Java prior to Java SE 8. Note that from Java SE 8 onwards, users are asked to migrate to java. time (JSR-310) - a core part of the JDK which replaces this project.

What is Joda-Time format?

Joda-Time provides a comprehensive formatting system. There are two layers: High level - pre-packaged constant formatters. Mid level - pattern-based, like SimpleDateFormat. Low level - builder.


1 Answers

Your code is fundamentally broken, because a Date object can't be "converted" between time zones - it represents an instant in time. getTime() returns the time in millis since the UTC Unix epoch. A Date doesn't depend on a time zone, so the idea of converting an instance of Date from one time zone to another is meaningless. It's a bit like converting an int from "base 10" to "base 16" - bases only make any sense when you think about a representation in digits rather than the fundamental number.

You should be using LocalDateTime to represent date/times without a fixed time zone, or DateTime to represent date/times with a specific time zone, or Instant to represent the same sort of concept as java.util.Date.

Once you use the appropriate types, I'm sure you won't have any problems.

EDIT: If your actual code is using a Calendar with the right time zone, you don't need to do anything to convert that to UTC. Just call calendar.getTime() and it will give you the appropriate Date.

For example:

    // Display all Date values as UTC for convenience
    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

    // November 6st 2010, 9:00pm (DST is still active until
    // early AM november 7th)
    Calendar nov6 = Calendar.getInstance();
    nov6.setTimeZone(TimeZone.getTimeZone("US/Arizona"));
    nov6.set(Calendar.HOUR_OF_DAY, 21);
    nov6.set(Calendar.MINUTE, 0);
    nov6.set(Calendar.SECOND, 0);
    nov6.set(Calendar.YEAR, 2010);
    nov6.set(Calendar.MONTH, 10); // November
    nov6.set(Calendar.DATE, 6);

    // Prints Sun Nov 07 04:00:00 UTC 2010 which is correct
    System.out.println("Local Nov6 = " + nov6.getTime());

Basically it's not clear to me why you're trying to convert from US/Arizona to US/Pacific when you talk about trying to save to UTC...

like image 82
Jon Skeet Avatar answered Oct 16 '22 10:10

Jon Skeet