Como lidar com TimeZones de calendário usando Java?


92

Eu tenho um valor de carimbo de data / hora que vem do meu aplicativo. O usuário pode estar em qualquer fuso horário local.

Como essa data é usada para um WebService que assume que a hora fornecida está sempre em GMT, preciso converter o parâmetro do usuário de, digamos, (EST) para (GMT). Aqui está o kicker: o usuário está alheio ao seu TZ. Ele insere a data de criação que deseja enviar ao WS, então o que eu preciso é:

O usuário insere: 01/05/2008 18:12 (EST)
O parâmetro para o WS deve ser : 01/05/2008 18:12 (GMT)

Eu sei que TimeStamps sempre devem estar em GMT por padrão, mas ao enviar o parâmetro, embora eu tenha criado meu calendário a partir do TS (que deveria estar em GMT), as horas estão sempre desligadas, a menos que o usuário esteja em GMT. o que estou perdendo?

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

Com o código anterior, é o que obtenho como resultado (formato curto para facilitar a leitura):

[1º de maio de 2008 23h12]


2
Por que você está apenas mudando o fuso horário e não convertendo a data / hora junto com ele?
Spencer Kormos

1
É uma verdadeira dor pegar uma data Java que está em um fuso horário e obter essa data em outro fuso horário. Ou seja, pegue 5PM EDT e 5PM PDT.
mtyson

Respostas:


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

Esta é a saída se eu passar a hora atual ("12:09:05 EDT" de Calendar.getInstance()) em:

DEBUG - calendário de entrada tem data [Thu Out 23 12:09:05 EDT 2008]
DEBUG - offset é -14400000
DEBUG - Criada GMT cal com data [Thu Out 23 08:09:05 EDT 2008]

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

A parte confusa aqui é que Calendar.getTime()retorna um Dateem seu fuso horário atual, e também que não há nenhum método para modificar o fuso horário de um calendário e ter a data subjacente rolada também. Dependendo do tipo de parâmetro que seu serviço da web usa, você pode querer apenas ter o acordo WS em termos de milissegundos a partir da época.


11
Você não deveria subtrair em offsetFromUTCvez de adicioná-lo? Usando o seu exemplo, se 12:09 GMT for 8:09 EDT (o que é verdade), e o usuário inserir "12:09 EDT", o algoritmo deverá gerar "16:09 GMT", em minha opinião.
DzinX de

29

Obrigado a todos por responder. Após uma investigação mais aprofundada, cheguei à resposta certa. Conforme mencionado por Skip Head, o TimeStamped que eu estava recebendo do meu aplicativo estava sendo ajustado para o TimeZone do usuário. Portanto, se o usuário inserisse 18h12 (EST), eu receberia 14h12 (GMT). O que eu precisava era de uma forma de desfazer a conversão para que a hora inserida pelo usuário fosse a hora que enviei para a solicitação do WebServer. Veja como eu fiz isso:

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

A saída do código é: (Usuário inserido 01/05/2008 18:12 (EST)

Fuso
horário do usuário atual: EST Compensação atual de GMT (em horas): - 4 (Normalmente -5, exceto se o horário de verão é ajustado)
TS de ACP: 2008-05-01 14: 12: 00.0
Data do calendário convertida de TS usando GMT e US_EN Local : 01/05/08 18:12 (GMT)


20

Você diz que a data é usada em conexão com serviços da web, então presumo que seja serializada em uma string em algum ponto.

Se for esse o caso, você deve dar uma olhada no método setTimeZone da classe DateFormat. Isso determina qual fuso horário será usado ao imprimir o carimbo de hora.

Um exemplo simples:

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
Não relisei SDF Tinha seu próprio fuso horário, perguntando por que alterar o fuso horário do calendário parece não ter efeito!
Chris.Jenkins

TimeZone.getTimeZone ("UTC") não é válido porque o UTC não está em AvailableIDs () ... Deus sabe por quê
childno͡.de

TimeZone.getTimeZone ("UTC") está disponível em minha máquina. É dependente de JVM?
CodeClimber

2
Ótima ideia com o método setTimeZone (). Você me salvou de muitas dores de cabeça, muito obrigado!
Bogdan Zurac

1
Exatamente o que eu precisava! Você pode definir o fuso horário do servidor no formatador e depois, ao convertê-lo para o formato de calendário, não precisa se preocupar. Melhor método de longe! para getTimeZone, pode ser necessário usar o formato Ex: "GMT-4: 00" para ETD
Michael Kern

12

Você pode resolver isso com 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"));

cuidado com isso, entretanto, se você estiver usando o hibernate 4, que não é diretamente compatível sem dependência lateral e configuração extra. No entanto, foi a abordagem mais rápida e fácil de usar para a 3ª versão.
Aubergine

8

Parece que seu TimeStamp está sendo definido para o fuso horário do sistema de origem.

Está obsoleto, mas deve funcionar:

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

A maneira não obsoleta é usar

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

mas isso precisaria ser feito no lado do cliente, já que esse sistema sabe em que fuso horário está.


7

Método para converter de um timeZone para outro (provavelmente 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;
}

6

Os objetos Date e Timestamp ignoram o fuso horário: eles representam um certo número de segundos desde a época, sem se comprometer com uma interpretação particular daquele instante como horas e dias. Os fusos horários entram na imagem apenas em GregorianCalendar (não diretamente necessário para esta tarefa) e SimpleDateFormat , que precisam de um deslocamento de fuso horário para converter entre campos separados e valores de data (ou longos ).

O problema do OP está logo no início de seu processamento: o usuário insere horas, que são ambíguas, e são interpretadas no fuso horário local, não GMT; neste ponto, o valor é "6:12 EST" , que pode ser facilmente impresso como "11,12 GMT" ou qualquer outro fuso horário, mas nunca mudará para "6,12 GMT" .

Não há como fazer com que o SimpleDateFormat que analisa "06:12" como "HH: MM" (padronizando para o fuso horário local) seja UTC como padrão; SimpleDateFormat é um pouco inteligente demais para seu próprio bem.

No entanto, você pode convencer qualquer instância de SimpleDateFormat a usar o fuso horário correto se colocá-lo explicitamente na entrada: basta anexar uma string fixa ao recebido (e adequadamente validado) "06:12" para analisar "06:12 GMT" como "HH: MM z" .

Não há necessidade de configuração explícita de campos GregorianCalendar ou de recuperar e usar fuso horário e ajustes de horário de verão.

O verdadeiro problema é segregar entradas que são padronizadas para o fuso horário local, entradas que são padronizadas para UTC e entradas que realmente requerem uma indicação explícita de fuso horário.


4

Algo que funcionou para mim no passado foi determinar o deslocamento (em milissegundos) entre o fuso horário do usuário e o GMT. Depois de obter o deslocamento, você pode simplesmente adicionar / subtrair (dependendo de como a conversão está indo) para obter o tempo apropriado em qualquer fuso horário. Eu normalmente faria isso definindo o campo de milissegundos de um objeto Calendar, mas tenho certeza de que você poderia aplicá-lo facilmente a um objeto timestamp. Aqui está o código que uso para obter o deslocamento

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

timezoneId é o id do fuso horário do usuário (como EST).


9
Usando o deslocamento bruto, você está ignorando o DST.
Werner Lehmann

1
Exatamente, esse método dará resultados incorretos por meio ano. A pior forma de erro.
Marinheiro Danubiano de

1

java.time

A abordagem moderna usa as classes java.time que suplantaram as classes legadas e problemáticas de data e hora agrupadas com as primeiras versões do Java.

A java.sql.Timestampclasse é uma daquelas classes legadas. Não é mais necessário. Em vez disso, use Instantou outras classes java.time diretamente com seu banco de dados usando JDBC 4.2 e posterior.

A Instantclasse representa um momento na linha do tempo em UTC com uma resolução de nanossegundos (até nove (9) dígitos de uma fração decimal).

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

Se você precisar interoperar com um existente Timestamp, converta imediatamente em java.time por meio dos novos métodos de conversão adicionados às classes antigas.

Instant instant = myTimestamp.toInstant() ;

Para ajustar em outro fuso horário, especifique o fuso horário como um ZoneIdobjeto. Especifique um nome próprio fuso horário no formato continent/region, como America/Montreal, Africa/Casablanca, ou Pacific/Auckland. Nunca use os pseudo-fusos de 3-4 letras, como ESTou ISTporque eles não são fusos horários verdadeiros, não padronizados e nem mesmo únicos (!).

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

Aplicar ao Instantpara produzir um ZonedDateTimeobjeto.

ZonedDateTime zdt = instant.atZone( z ) ;

Para gerar uma string para exibir ao usuário, pesquise Stack Overflow para DateTimeFormatterencontrar muitas discussões e exemplos.

Sua pergunta é realmente sobre ir na outra direção, da entrada de dados do usuário aos objetos de data e hora. Geralmente é melhor dividir a entrada de dados em duas partes, uma data e uma hora do dia.

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

Sua pergunta não está clara. Você quer interpretar a data e a hora inseridas pelo usuário como sendo UTC? Ou em outro fuso horário?

Se você quis dizer UTC, crie um OffsetDateTimecom um deslocamento usando a constante para UTC ZoneOffset.UTC,.

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

Se você quis dizer outro fuso horário, combine junto com um objeto de fuso horário, a ZoneId. Mas qual fuso horário? Você pode detectar um fuso horário padrão. Ou, se for crítico, você deve confirmar com o usuário para ter certeza de sua intenção.

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

Para obter um objeto mais simples que está sempre em UTC por definição, extraia um Instant.

Instant instant = odt.toInstant() ;

…ou…

Instant instant = zdt.toInstant() ; 

Envie para seu banco de dados.

myPreparedStatement.setObject(  , instant ) ;

Sobre java.time

A estrutura java.time é integrada ao Java 8 e posterior. Essas classes suplantar os velhos problemáticos legados aulas de data e hora, como java.util.Date, Calendar, e SimpleDateFormat.

O projeto Joda-Time , agora em modo de manutenção , aconselha a migração para as classes java.time .

Para saber mais, consulte o tutorial Oracle . E pesquise no Stack Overflow para muitos exemplos e explicações. A especificação é JSR 310 .

Onde obter as classes java.time?

  • Java SE 8 , Java SE 9 e posterior
    • Construídas em.
    • Parte da API Java padrão com uma implementação agrupada.
    • Java 9 adiciona alguns recursos e correções menores.
  • Java SE 6 e Java SE 7
    • Grande parte da funcionalidade java.time é portada para Java 6 e 7 no ThreeTen-Backport .
  • Android
    • Versões posteriores de implementações de pacotes Android das classes java.time.
    • Para anteriormente Android, o ThreeTenABP projeto adapta ThreeTen-Backport (mencionados acima). Consulte Como usar o ThreeTenABP… .

O projeto ThreeTen-Extra estende java.time com classes adicionais. Este projeto é um campo de provas para possíveis adições futuras ao java.time. Você pode encontrar algumas classes úteis aqui, como Interval, YearWeek, YearQuarter, e mais .

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.