¿Cómo manejar calendarios TimeZones usando Java?

Tengo un valor Timestamp que proviene de mi aplicación. El usuario puede estar en cualquier TimeZone local.

Como esta fecha se usa para un servicio web que asume que el tiempo dado 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. Ingresa la fecha de creación que desea enviar al WS, entonces lo que necesito es:

El usuario ingresa: 5/1/2008 6:12 PM (EST)
El parámetro para WS debe ser : 5/1/2008 6:12 PM (GMT)

Sé que TimeStamps siempre se supone que están en GMT de forma predeterminada, 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 obtengo como resultado (Formato corto para facilitar la lectura):

[1 de mayo de 2008, 11:12 p.m.]

 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á el resultado si paso la hora actual (“12:09:05 EDT” desde Calendar.getInstance() ) en:

DEPURACIÓN – el calendario de entrada tiene fecha [jue 23 de octubre 12:09:05 EDT 2008]
DEBUG – offset es -14400000
DEBUG – Calcuta GMT creada con fecha [jue 23 de octubre 08:09:05 EDT 2008]

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

La parte confusa aquí es que Calendar.getTime() devuelve una 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 que la fecha subyacente también se haya transferido. 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.

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. Así es como logré esto:

 // 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 ingresado 5/1/2008 6:12 PM (EST)

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

Usted dice que la fecha se usa en conexión con servicios web, así que supongo que se serializa en una cadena en algún momento.

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

Un simple ejemplo:

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

Puedes resolverlo 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")); 

Método para convertir de una zona horaria a otra (probablemente funcione :)).

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

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

Esto está en desuso, pero debería funcionar:

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

La forma no desaprovechada es usar

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

pero eso tendría que hacerse en el lado del cliente, ya que ese sistema sabe en qué zona horaria se encuentra.

Los objetos Date y Timestamp son indiferentes en la zona horaria: representan un cierto número de segundos desde la época, sin comprometerse con una interpretación particular de ese instante como horas y días. Las zonas horarias solo entran en la imagen en GregorianCalendar (no se necesita directamente para esta tarea) y SimpleDateFormat , que necesita un desplazamiento de zona horaria para convertir entre campos separados y valores de Fecha (o de larga duración ).

El problema del PO 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 “6:12 EST” , que se puede imprimir fácilmente como “11.12 GMT” o cualquier otra zona horaria, pero nunca cambiará a “6.12 GMT” .

No hay forma de hacer que SimpleDateFormat analice “06:12” como “HH: MM” (valor predeterminado en la zona horaria local) de forma predeterminada a UTC; SimpleDateFormat es demasiado inteligente para su propio bien.

Sin embargo, puede convencer a cualquier instancia de SimpleDateFormat de usar la zona horaria correcta si la coloca explícitamente en la entrada: simplemente agregue una cadena fija al “06:12” recibido (y adecuadamente validado) para analizar “06:12 GMT” como “HH: MM z” .

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

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

Algo que me ha funcionado en el pasado fue determinar el desplazamiento (en milisegundos) entre la zona horaria del usuario y GMT. Una vez que tenga el desplazamiento, puede simplemente sumr / 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 uso para obtener el desplazamiento

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

timezoneId es la identificación de la zona horaria del usuario (como EST).

java.time

El enfoque moderno usa las clases java.time que suplantaron las problemáticas clases heredadas de fecha y hora incluidas 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 nanosegundos (hasta nueve (9) dígitos de una fracción decimal).

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

Si debe interoperar con una Timestamp existente, conviértala inmediatamente en java.time a través de los nuevos métodos de conversión agregados a las clases anteriores.

 Instant instant = myTimestamp.toInstant() ; 

Para ajustar en otra zona horaria, especifique la zona horaria como un objeto ZoneId . Especifique un nombre de zona horaria adecuada en el formato 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 no son zonas horarias verdaderas, no están estandarizadas y ni siquiera son únicas (!).

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

Aplicar al Instant para producir un objeto ZonedDateTime .

 ZonedDateTime zdt = instant.atZone( z ) ; 

Para generar una cadena para mostrar al usuario, busque Stack Overflow para DateTimeFormatter para encontrar muchas discusiones y ejemplos.

Su pregunta se trata realmente 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:ma" , Locale.US ) ) ; 

Tu pregunta no está 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 se refería a otra zona horaria, combine junto con un objeto de zona horaria, un 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 siempre está en UTC por definición, extrae un Instant .

 Instant instant = odt.toInstant() ; 

…o…

 Instant instant = zdt.toInstant() ; 

Enviar a su base de datos

 myPreparedStatement.setObject( … , instant ) ; 

Acerca de java.time

El marco java.time está integrado en Java 8 y posterior. Estas clases suplantan a las problemáticas antiguas clases heredadas de fecha y hora como java.util.Date , Calendar y SimpleDateFormat .

El proyecto Joda-Time , ahora en modo de mantenimiento , aconseja la migración a las clases java.time .

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

¿Dónde obtener las clases de java.time?

  • Java SE 8 , Java SE 9 y posterior
    • Incorporado.
    • Parte de la API Java estándar con una implementación integrada.
    • Java 9 agrega algunas características y correcciones menores.
  • Java SE 6 y Java SE 7
    • Gran parte de la funcionalidad de java.time se transfiere a Java 6 y 7 en ThreeTen-Backport .
  • Androide
    • Versiones posteriores de las implementaciones del paquete de Android de las clases java.time.
    • Para Android anterior, el proyecto ThreeTenABP adapta ThreeTen-Backport (mencionado anteriormente). Consulte Cómo utilizar ThreeTenABP ….

El proyecto ThreeTen-Extra amplía 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 más .