Como lidar com TimeZones calendário usando Java?
Pergunta
Eu tenho um valor da hora que vem da minha aplicação. O usuário pode estar em qualquer fuso horário local.
Uma vez que esta data é usada para um WebService que assume o tempo fornecido é sempre em GMT, eu tenho uma necessidade de converter parâmetro do usuário a partir digamos (EST) para (GMT). Aqui está o kicker: O usuário é alheio à sua TZ. Ele entra a data de criação que ele quer enviar para o WS, então o que eu preciso é:
O usuário digita: 5/1/2008 6:12 (EST)
O parâmetro para as necessidades WS ser : 5/1/2008 18:12 (GMT)
Eu sei TimeStamps são sempre deveria estar em GMT por padrão, mas ao enviar o parâmetro, mesmo que eu criei o meu Calendário dos TS (que é suposto estar em GMT), as horas são sempre desligado a menos que o usuário é em GMT. O que eu estou ausente?
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, isso é o que eu recebo como resultado (Formato Curto para facilitar a leitura):
[01 maio de 2008 11:12]
Solução 2
Obrigado a todos por responder. Após uma investigação mais aprofundada cheguei à resposta certa. Como mencionado por Skip Head, o timestamped eu estava ficando do meu aplicativo estava sendo ajustado para TimeZone do usuário. Portanto, se o usuário entrou 6:12 PM (EST) gostaria de obter 14:12 (GMT). O que eu precisava era uma maneira de desfazer a conversão para que o tempo digitado pelo usuário é o momento que enviei ao pedido WebServer. Aqui está como eu conseguido 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 é: (User entrou 5/1/2008 6:12 (EST)
Usuário do atual fuso horário: EST
Corrente de GMT (em horas): - 4 (-5 Normalmente, excepto é ajustado DST)
TS de ACP: 2008-05-01 14: 12: 00.0
Calendário Data convertido a partir de TS usando GMT e US_EN Locale: 5/1/08 06:12 (GMT)
Outras dicas
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;
}
Aqui está a saída se eu passar o tempo atual ( "12:09:05 EDT" de Calendar.getInstance()
) em:
DEBUG - calendário de entrada tem data [Qui 23 outubro 12:09:05 EDT 2008]
DEBUG - offset é -14400000
DEBUG - Criado GMT cal com data [Qui 23 outubro 08:09:05 EDT 2008]
12:09:05 GMT é 08:09:05 EDT.
A parte confusa aqui é que Calendar.getTime()
Retorna um Date
em seu fuso horário atual, e também que não existe um método para modificar o fuso horário de um calendário e ter a data subjacente rolou também. Dependendo do tipo de parâmetro seu serviço web leva, a sua pode apenas querer ter o negócio WS em termos de milissegundos de época.
Você diz que a data é usada em conexão com serviços web, assim que eu supor que é serializado em uma corda em algum ponto.
Se este for o caso, você deve dar uma olhada no método setTimeZone da classe DateFormat. Este ditames que o fuso horário que será usado ao imprimir o carimbo de tempo.
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());
Você pode resolvê-lo com Joda Tempo :
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"));
Parece que o seu TimeStamp está sendo definido para o fuso horário do sistema de origem.
Esta é obsoleto, mas deve funcionar:
cal.setTimeInMillis(ts_.getTime() - ts_.getTimezoneOffset());
A forma não obsoleta é usar
Calendar.get(Calendar.ZONE_OFFSET) + Calendar.get(Calendar.DST_OFFSET)) / (60 * 1000)
mas que precisa ser feito no lado do cliente, uma vez que o sistema sabe o fuso horário é no.
Método para converter de um fuso-horário para outra (provavelmente ele 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;
}
Data ??strong> e Timestamp objetos são fuso horário-alheio: eles representam um determinado número de segundos desde a época, sem se comprometer com uma interpretação particular desse instante, como horas e dias . Fusos horários entram em cena apenas em GregorianCalendar (não diretamente necessários para esta tarefa) e SimpleDateFormat , que precisa de uma compensação para converter entre os campos e Data separados (fuso horário ou longa ) valores.
O problema do OP é logo no início de sua transformação: as horas de entradas de usuários, que são ambíguas, e eles são interpretados na GMT não fuso horário local,; Neste ponto, o valor é "06:12 EST" , que pode ser facilmente impresso como "11.12 GMT" ou qualquer outro fuso horário, mas nunca vai mudar e "6.12 GMT" .
Não há nenhuma maneira de fazer o SimpleDateFormat que analisa "6:12" como "HH: MM" (falta ao local, fuso horário) padrão para UTC em vez; SimpleDateFormat é um pouco esperto demais para seu próprio bem.
No entanto, você pode convencer qualquer SimpleDateFormat instância para usar o fuso horário certo, se você colocá-lo explicitamente na entrada: basta adicionar uma corda fixa ao recebido (e adequadamente validado) " 06:12 " para analisar " 06:12 GMT " como " HH: MM z ".
Não há necessidade de definição explícita de GregorianCalendar campos ou de recuperação e usando fuso horário e horário de verão deslocamentos de tempo.
O problema real é segregar insumos que padrão para o fuso horário local, insumos que padrão a UTC e insumos que realmente exigem uma indicação fuso horário explícito.
Algo que tem trabalhado para mim no passado foi determinar o offset (em milissegundos) entre fuso horário do usuário e GMT. Depois de ter o deslocamento, você pode simplesmente adicionar / subtrair (dependendo da maneira que a conversão está acontecendo) para obter o momento apropriado em qualquer fuso horário. Eu costumo fazer isso definindo o campo milissegundos de um objeto de calendário, mas eu tenho certeza que você poderia facilmente aplicá-la a um objeto timestamp. Aqui está o código que eu uso para obter o deslocamento
int offset = TimeZone.getTimeZone(timezoneId).getRawOffset();
TimeZoneID é o ID do fuso horário do usuário (como EST).
java.time
A abordagem moderna usa os java.time classes que suplantaram os legados aulas de data e hora problemáticos empacotados com as primeiras versões do Java.
A classe java.sql.Timestamp
é uma dessas classes de legados. Não é mais necessário. Em vez disso use Instant
ou outro java.time classes diretamente com seu banco de dados usando JDBC 4.2 e posterior.
O Instant
classe representa um momento na linha do tempo em UTC com uma resolução de nanossegundos (até nove (9) dígitos de uma fracção decimal).
Instant instant = myResultSet.getObject( … , Instant.class ) ;
Se você deve interoperar com um Timestamp
existente, converter imediatamente em java.time através dos novos métodos de conversão acrescentado as classes de idade.
Instant instant = myTimestamp.toInstant() ;
Para ajustar em outro fuso horário, especifique o fuso horário como um objeto ZoneId
. Especifique um tempo apropriado nome de zona no formato de continent/region
, como America/Montreal
, Africa/Casablanca
, ou Pacific/Auckland
. Nunca use os pseudo-zonas 3-4 carta como EST
ou IST
como eles são não fusos horários verdadeiros, não padronizados, e nem mesmo únicos (!).
ZoneId z = ZoneId.of( "America/Montreal" ) ;
Aplicar ao Instant
para produzir um objeto ZonedDateTime
.
ZonedDateTime zdt = instant.atZone( z ) ;
Para gerar uma seqüência de exibição para o usuário, pesquisa Stack Overflow para DateTimeFormatter
encontrar muitas discussões e exemplos.
A sua pergunta é realmente sobre ir na outra direção, a partir de dados do usuário-entrada para os objetos de data e hora. Geralmente melhor para quebrar o seu de 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 é clara. Você quer interpretar a data ea hora inseridas pelo usuário para a UTC? Ou em outro fuso horário?
Se você quis dizer UTC, criar um OffsetDateTime
com um deslocamento usando a constante para UTC, ZoneOffset.UTC
.
OffsetDateTime odt = OffsetDateTime.of( ld , lt , ZoneOffset.UTC ) ;
Se você significou outro fuso horário, combinam juntamente com uma zona de objeto de tempo, um ZoneId
. Mas qual fuso horário? Você pode detectar um fuso horário padrão. Ou, se crítico, você deve confirmar com o usuário para ter certeza da sua intenção.
ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;
Para obter um objeto simples que está sempre em UTC, por definição, extrair um Instant
.
Instant instant = odt.toInstant() ;
... ou ...
Instant instant = zdt.toInstant() ;
Enviar para o seu banco de dados.
myPreparedStatement.setObject( … , instant ) ;
Sobre java.time
O java.time quadro é construído em Java 8 e posterior. Essas classes suplantar o problemático idade legados aulas de data e hora, como java.util.Date
, Calendar
, e SimpleDateFormat
.
O Joda-Time projeto, agora em manutenção mode , aconselha a migração para a java.time classes.
Para saber mais, consulte a a Oracle Tutorial . E busca Stack Overflow para muitos exemplos e explicações. Especificação é JSR 310 .
Onde obter as classes java.time?
- Java SE 8 , Java SE 9 , e mais tarde
- Built-in.
- Parte da API Java padrão com uma implementação empacotado.
- Java 9 adiciona alguns recursos menores e correções.
- Java SE 6 e Java SE 7
- Grande parte da funcionalidade java.time é back-portados para o Java 6 e 7 em ThreeTen-Backport .
- Android
- As versões posteriores do Android pacote implementações das classes java.time.
- Para anteriormente Android, o ThreeTenABP adapta projeto ThreeTen-Backport (mencionados acima). Consulte Como usar ThreeTenABP ... .
O ThreeTen-Extra projeto estende java.time com aulas adicionais. Este projeto é um campo de provas para possíveis adições futuras para java.time. Você pode encontrar algumas classes úteis aqui, como Interval
, YearWeek
, YearQuarter
, e mais .