¿Cómo manejar las zonas horarias del calendario usando Java?

92

Tengo un valor de marca de tiempo que proviene de mi aplicación. El usuario puede estar en cualquier zona horaria local determinada.

Dado que esta fecha se usa para un WebService que asume que la hora dada siempre está en GMT, tengo la necesidad de convertir el parámetro del usuario de digamos (EST) a (GMT). Aquí está el truco: el usuario es ajeno a su TZ. Ingresa la fecha de creación que quiere enviar a WS, entonces lo que necesito es:

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

Sé que las marcas de tiempo siempre deben estar 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 una lectura fácil):

[1 de mayo de 2008 11:12 p. M.]

Jorge Valois
fuente
2
¿Por qué solo cambia la zona horaria y no convierte la fecha / hora junto con ella?
Spencer Kormos
1
Es una verdadera molestia tomar una fecha de Java que está en una zona horaria y obtener esa fecha en otra zona horaria. Es decir, tome las 5 p.m. EDT y obtenga las 5 p.m. PDT.
mtyson

Respuestas:

62
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: el calendario de entrada tiene fecha [Thu Oct 23 12:09:05 EDT 2008]
DEBUG: el desplazamiento es -14400000
DEBUG: se ha creado la calibración GMT con fecha [Thu Oct 23 08:09:05 EDT 2008]

12:09:05 GMT son las 8:09:05 EDT.

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

mate b
fuente
11
¿No debería restar en offsetFromUTClugar de sumarlo? Usando su ejemplo, si las 12:09 GMT son las 8:09 EDT (que es cierto), y el usuario ingresa "12:09 EDT", el algoritmo debería generar "16:09 GMT", en mi opinión.
DzinX
29

Gracias a todos por responder. Después de una investigación más profunda, obtuve la respuesta correcta. Como mencionó Skip Head, el TimeStamped que obtenía de mi aplicación se estaba ajustando al TimeZone del usuario. Entonces, si el usuario ingresó a las 6:12 p.m. (EST), obtendría 2:12 p.m. (GMT). Lo que necesitaba era una forma de deshacer la conversión para que la hora ingresada por el usuario sea la hora que envié a la solicitud del WebServer. 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: (El usuario ingresó el 5/1/2008 6:12 PM (EST)

Zona horaria del usuario actual: EST
Desplazamiento actual de GMT (en horas): - 4 (Normalmente -5, excepto que se ajusta el DST)
TS de ACP: 2008-05-01 14: 12: 00.0
Fecha de calendario convertida de TS usando GMT y US_EN Locale : 1/5/08 6:12 p.m. (GMT)

Jorge Valois
fuente
20

Dice que la fecha se usa en relación con 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 método setTimeZone de la clase DateFormat. Esto dicta qué zona horaria se utilizará 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());
Henrik Aasted Sørensen
fuente
4
No realicé SDF Tenía su propia zona horaria, preguntándose por qué el cambio de Calendar TimeZone parece no tener ningún efecto.
Chris.Jenkins
TimeZone.getTimeZone ("UTC") no es válido ya que UTC no está en AvailableIDs () ... Dios sabe por qué
childno͡.de
TimeZone.getTimeZone ("UTC") está disponible en mi máquina. ¿Depende de JVM?
CodeClimber
2
Impresionante idea con el método setTimeZone (). Me ahorraste muchos dolores de cabeza, ¡muchas gracias!
Bogdan Zurac
1
¡Justo lo que necesitaba! Puede configurar la zona horaria del servidor en el formateador y luego, cuando lo convierte al formato de calendario, no tiene nada de qué preocuparse. ¡El mejor método con diferencia! para getTimeZone, es posible que deba usar el formato Ej .: "GMT-4: 00" para ETD
Michael Kern
12

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"));
Vitalii Fedorenko
fuente
Sin embargo, tenga cuidado con esto si está utilizando hibernate 4, que no es directamente compatible sin dependencia lateral y configuración adicional. Sin embargo, fue el enfoque más rápido y fácil de usar para la tercera versión.
Berenjena
8

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

Esto está desaprobado, pero debería funcionar:

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

La forma no obsoleta es usar

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

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

Saltar cabeza
fuente
7

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;
}
Helpa
fuente
6

Los objetos Date y Timestamp son ajenos a 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 ingresan a la imagen solo en GregorianCalendar (no se necesita directamente para esta tarea) y SimpleDateFormat , que necesitan un desplazamiento de zona horaria para convertir entre campos separados y valores de fecha (o largos ).

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 "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 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 instancia de SimpleDateFormat para que use la zona horaria correcta si la coloca explícitamente en la entrada: simplemente agregue una cadena fija a las "06:12" recibidas (y adecuadamente validadas) para analizar "06:12 GMT" como "HH: MM z" .

No hay necesidad de establecer explícitamente los campos GregorianCalendar o de recuperar y usar la zona horaria y las compensaciones del horario de verano.

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

user412090
fuente
4

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 sumar / restar (dependiendo de en qué dirección vaya la conversión) para obtener el tiempo apropiado en cualquier zona horaria. Por lo general, lograría esto configurando el campo de milisegundos de un objeto 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 la compensación

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

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

Adán
fuente
9
Al utilizar el desplazamiento sin procesar, ignora el horario de verano.
Werner Lehmann
1
Exactamente, este método dará resultados incorrectos durante medio año. La peor forma de error.
Marinero del Danubio
1

java.time

El enfoque moderno utiliza las clases java.time que suplantaron las problemáticas clases de fecha y hora heredadas incluidas con las primeras versiones de Java.

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

La Instantclase 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 uno existente Timestamp, conviértalo inmediatamente a java.time mediante los nuevos métodos de conversión agregados a las clases antiguas.

Instant instant = myTimestamp.toInstant() ;

Para ajustar a otra zona horaria, especifique la zona horaria como un ZoneIdobjeto. Especificar un nombre de zona horaria correcta en el formato de continent/region, por ejemplo America/Montreal, Africa/Casablancao Pacific/Auckland. Nunca use las pseudozonas de 3-4 letras como ESTo ISTya que no son zonas horarias verdaderas, no están estandarizadas y ni siquiera son únicas (!).

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

Aplicar al Instantpara producir un ZonedDateTimeobjeto.

ZonedDateTime zdt = instant.atZone( z ) ;

Para generar una cadena para mostrar al usuario, busque Stack Overflow para DateTimeFormatterencontrar 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. Generalmente es mejor dividir la entrada 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á clara. ¿Quiere interpretar que la fecha y la hora introducidas por el usuario están en UTC? ¿O en otra zona horaria?

Si nos referimos UTC, crear una OffsetDateTimecon un desplazamiento utilizando la constante de 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, a ZoneId. ¿Pero en qué zona horaria? Es posible que detecte 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, extraiga un archivo Instant.

Instant instant = odt.toInstant() ;

…o…

Instant instant = zdt.toInstant() ; 

Envíe a su base de datos.

myPreparedStatement.setObject(  , instant ) ;

Acerca de java.time

El marco java.time está integrado en Java 8 y versiones posteriores. Estas clases suplantar la vieja problemáticos heredados clases 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 java.time?

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

El proyecto ThreeTen-Extra extiende java.time con clases adicionales. Este proyecto es un campo de pruebas para posibles adiciones futuras a java.time. Usted puede encontrar algunas clases útiles aquí, como Interval, YearWeek, YearQuarter, y más .

Albahaca Bourque
fuente