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));
Da开发者_开发问答te 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);
}
}
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...
精彩评论