2008-10-23 1596 views
87

Tengo un valor Timestamp que proviene de mi aplicación. El usuario puede estar en cualquier TimeZone local.¿Cómo se maneja el calendario TimeZones usando Java?

Como esta fecha se usa para un servicio web que asume que la hora siempre está en GMT, necesito convertir el parámetro del usuario de decir (EST) a (GMT). Aquí está el truco: el usuario es ajeno a su TZ. Entra en la fecha de creación que quiere enviar al WS, por lo que lo que necesito es:

usuario entra: 5/1/2008 18:12 (EST)
El parámetro a las necesidades WS ser: 5/1/2008 6:12 PM (GMT)

Sé que las marcas de tiempo siempre se supone que están en GMT por defecto, pero al enviar el parámetro, aunque creé mi calendario desde el TS (que se supone que está en GMT), las horas siempre están apagadas a menos que el usuario esté en GMT. ¿Qué me estoy perdiendo?

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; 
} 

Con el Código anterior, esto es lo que me pasa como resultado (formato breve para facilitar la lectura):

[1 de mayo, 2008 23:12]

+2

¿Por qué solo está cambiando la zona horaria y no está convirtiendo la fecha/hora junto con ella? –

+1

Es realmente molesto tomar una fecha Java que está en una zona horaria, y obtener * esa * fecha en otra zona horaria. IE, tome 5PM EDT y obtenga 5PM PDT. – mtyson

Respuesta

28

Gracias a todos por responder. Después de una nueva investigación, llegué a la respuesta correcta. Como lo mencionó Skip Head, el TimeStamped que obtenía de mi aplicación se estaba ajustando al TimeZone del usuario. Entonces, si el usuario ingresara a las 6:12 p.m. (EST), obtendría las 2:12 p.m. (GMT). Lo que necesitaba era una forma de deshacer la conversión para que el tiempo ingresado por el usuario sea el tiempo que envié a la solicitud del servidor web. He aquí cómo lo logré:

// 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())); 

La salida del código es: (Usuario entró 5/1/2008 18:12 (EST)

zona horaria

del usuario actual: EST
actual respecto a GMT (en horas): - 4 (Normalmente -5, excepto que se ajusta el horario de verano)
TS de ACP: 2008-05-01 14: 12: 00.0
Calendario Fecha de conversión de TS usando GMT y US_EN Configuración regional: 5/1/08 6: 12 PM (GMT)

4

algo que ha funcionado para mí en el pasado era determinar el desplazamiento (en milisegundos) entre la zona horaria del usuario y GMT. Una vez que tenga el desplazamiento, puede simplemente sumar/restar (dependiendo de la dirección de la conversión) para obtener el tiempo apropiado en cualquier zona horaria. Por lo general, lo conseguiría estableciendo el campo de milisegundos de un objeto de calendario, pero estoy seguro de que podría aplicarlo fácilmente a un objeto de marca de tiempo. Aquí está el código que utilizo para obtener el desplazamiento

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

TimeZoneID es el ID de la zona horaria del usuario (como EST).

+8

Al usar el desplazamiento sin formato, está ignorando el horario de verano. –

+1

Exactamente, este método arrojará resultados incorrectos durante la mitad del año. La peor forma de error. –

7

Parece que su TimeStamp se está configurando en la zona horaria del sistema de origen.

Esto es obsoleto, pero debería funcionar:

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

La forma no obsoleta es utilizar

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

pero que habría que hacer en el lado del cliente, ya que el sistema de sabe qué zona horaria se encuentra la que está.

58
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; 
} 

Aquí está la salida si paso la hora actual ("12:09:05 EDT" de Calendar.getInstance()) en:

DEBUG - calendario de entrada tiene fecha [23 de Mar Oct 2008 12:09:05 EDT]
DEBUG - offset es -14400000
DEBUG - Creado cal GMT con la fecha [23 de Mar Oct 08 : 09: 05 EDT 2008]

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

La parte confusa aquí es que Calendar.getTime() le devuelve un Date en su zona horaria actual, y también que no hay ningún método para modificar la zona horaria de un calendario y tener la fecha subyacente también.Dependiendo del tipo de parámetro que tome su servicio web, es posible que solo desee que la WS trate en términos de milisegundos de época.

+10

¿No deberías estar restando 'offsetFromUTC' en lugar de sumarlo? Usando su ejemplo, si 12:09 GMT es 8:09 EDT (que es cierto), y el usuario ingresa "12:09 EDT", el algoritmo debería emitir "16:09 GMT", en mi opinión. – DzinX

19

Usted dice que la fecha se usa en conexión wi Los servicios web, por lo que supongo que se serializa en una cadena en algún momento.

Si este es el caso, debería echar un vistazo al setTimeZone method de la clase DateFormat. Esto dicta qué zona horaria se usará al imprimir la marca de tiempo.

Un ejemplo sencillo:

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()); 
+4

No volvió a usar SDF. Tenía su propia zona horaria, preguntándose por qué cambiar Calendar TimeZone parece no tener ningún efecto. –

+0

TimeZone.getTimeZone ("UTC") no es válido porque UTC no está en AvailableIDs() ... Dios sabe por qué –

+0

TimeZone.getTimeZone ("UTC") está disponible en mi máquina. ¿Es dependiente de JVM? – CodeClimber

7

Método para la conversión de un timeZone a otra (probablemente funciona :)).

/** 
* 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; 
} 
12

se puede resolver con 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")); 
+0

, sin embargo, si está utilizando hibernate 4, que no es directamente compatible sin dependencia lateral y configuraciones adicionales. Sin embargo, fue el enfoque más rápido y fácil de usar para la tercera versión. – Aubergine

6

Fecha y Marca de tiempo objetos son zona horaria-ajeno: representan un cierto número de segundos desde la época, sin comprometerse a una interpretación particular de esa insta nt como horas y días. zonas horarias entran en escena sólo en GregorianCalendar (no es necesario directamente para esta tarea) y SimpleDateFormat, que necesitan un desplazamiento para convertir entre campos separados y fecha (o largos ) valores de zona horaria.

El problema del OP está justo al comienzo de su procesamiento: el usuario ingresa horas, que son ambiguas, y se interpretan en la zona horaria local, no GMT; en este punto el valor es "06:12 EST", que se puede imprimir fácilmente como "11.12 GMT" o cualquier otra zona horaria, pero nunca va a cambiar a "6.12 GMT".

No hay manera de hacer que el SimpleDateFormat que analiza "06:12" como "HH: MM" (por defecto a la zona horaria local) por defecto a UTC en su lugar; SimpleDateFormat es demasiado inteligente para su propio bien.

Sin embargo, puede convencer a cualquier SimpleDateFormat ejemplo, para utilizar la zona horaria correcta si lo pone explícitamente en la entrada: acaba de añadir una cadena fijada al recibido (y validado adecuadamente) "06:12" a parse "06:12 GMT" como "HH: MM z".

No hay necesidad de configuración explícita de los campos GregorianCalendar o de recuperar y utilizar las funciones de la zona horaria y del horario de verano.

El problema real es segregar las entradas que por defecto son la zona horaria local, las entradas que por defecto son UTC, y las entradas que realmente requieren una indicación de zona horaria explícita.

1

java.time

El enfoque moderno utiliza el java.time clases que suplantaron las clases heredadas de fecha y hora molestos que vienen con las primeras versiones de Java.

La clase java.sql.Timestamp es una de esas clases heredadas. Ya no es necesario. En su lugar, use Instant u otras clases de java.time directamente con su base de datos usando JDBC 4.2 y posterior.

La clase Instant representa un momento en la línea de tiempo en UTC con una resolución de nanoseconds (hasta nueve (9) dígitos de una fracción decimal).

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

Si debe interoperar con un Timestamp existente, convertir inmediatamente en java.time a través de los nuevos métodos de conversión añadido a las viejas clases.

Instant instant = myTimestamp.toInstant() ; 

Para ajustar a otra zona horaria, especifique la zona horaria como un objeto ZoneId. Especifique un proper time zone name en el formato de continent/region, como America/Montreal, Africa/Casablanca o Pacific/Auckland. Nunca use las pseudo-zonas de 3-4 letras como EST o IST ya que son no zonas horarias verdaderas, no estandarizadas, y ni siquiera únicas (!).

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

se aplican a la Instant para producir un objeto ZonedDateTime.

ZonedDateTime zdt = instant.atZone(z) ; 

para generar una cadena para ser presentado al usuario, búsqueda de desbordamiento de pila para DateTimeFormatter encontrar muchas discusiones y ejemplos.

Su pregunta es realmente acerca de ir en la otra dirección, desde la entrada de datos del usuario hasta los objetos de fecha y hora. Por lo general, es mejor romper su ingreso de datos en dos partes, una fecha y una hora del día.

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

Tu pregunta no está nada clara. ¿Desea interpretar la fecha y la hora ingresadas por el usuario para estar en UTC? ¿O en otra zona horaria?

Si se refería a UTC, cree un OffsetDateTime con un desplazamiento usando la constante para UTC, ZoneOffset.UTC.

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

Si usted significó otra zona horaria, se combinan junto con un objeto de zona horaria, una ZoneId. Pero, ¿qué zona horaria? Puede detectar una zona horaria predeterminada. O, si es crítico, debe confirmar con el usuario para estar seguro de su intención.

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

para obtener un objeto más simple que está siempre en UTC, por definición, extraer un Instant.

Instant instant = odt.toInstant() ; 

... o ...

Instant instant = zdt.toInstant() ; 

enviar a su base de datos.

myPreparedStatement.setObject(… , instant) ; 

Sobre java.time

El marco java.time está incorporado en Java 8 y versiones posteriores. Estas clases suplantan a las viejas y problemáticas clases de fecha y hora de legacy, como java.util.Date, Calendar, & SimpleDateFormat.

El proyecto Joda-Time, ahora en maintenance mode, recomienda la migración a las clases java.time.

Para obtener más información, consulte el Oracle Tutorial. Y busque Stack Overflow para obtener muchos ejemplos y explicaciones. La especificación es JSR 310.

¿Dónde obtener las clases java.time?

  • Java SE 8, Java SE 9, y más tarde
    • incorporado.
    • Parte de la API estándar de Java con una implementación integrada.
    • Java 9 agrega algunas características menores y correcciones.
  • Java SE 6 y Java SE 7
    • Gran parte de la funcionalidad de back-java.time está portado a Java 6 & 7 en ThreeTen-Backport.
  • Android
    • Las versiones posteriores de las implementaciones de haces de Android de las clases java.time.
    • Para Android anterior, el proyecto ThreeTenABP se adapta a ThreeTen-Backport (mencionado anteriormente). Ver How to use ThreeTenABP….

El proyecto se extiende ThreeTen-Extra java.time con clases adicionales. Este proyecto es un terreno de prueba para posibles adiciones futuras a java.time. Puede encontrar algunas clases útiles aquí, como Interval, YearWeek, YearQuarter y more.

Cuestiones relacionadas