Come gestire i fusi orari del calendario utilizzando Java?


92

Ho un valore Timestamp che proviene dalla mia applicazione. L'utente può trovarsi in qualsiasi fuso orario locale.

Poiché questa data viene utilizzata per un WebService che presuppone che l'ora indicata sia sempre GMT, ho bisogno di convertire il parametro dell'utente da say (EST) a (GMT). Ecco il kicker: l'utente è ignaro della sua TZ. Inserisce la data di creazione che vuole inviare al WS, quindi quello che mi serve è:

L'utente immette: 5/1/2008 6:12 PM (EST)
Il parametro per il WS deve essere : 5/1/2008 6:12 PM (GMT)

So che i timestamp dovrebbero sempre essere in GMT per impostazione predefinita, ma quando si invia il parametro, anche se ho creato il mio calendario dal TS (che dovrebbe essere in GMT), le ore sono sempre spente a meno che l'utente non sia in GMT. Cosa mi sto 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;
}

Con il codice precedente, questo è ciò che ottengo come risultato (formato breve per una facile lettura):

[1 maggio 2008 23:12]


2
Perché stai solo modificando il fuso orario e non convertendo la data / ora insieme ad esso?
Spencer Kormos

1
È un vero dolore prendere una data Java che si trova in un fuso orario e ottenere quella data in un altro fuso orario. IE, prendi 5PM EDT e ottieni 5PM PDT.
mtyson

Risposte:


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

Ecco l'output se passo l'ora corrente ("12:09:05 EDT" da Calendar.getInstance()) in:

DEBUG - il calendario di input ha una data [Thu Oct 23 12:09:05 EDT 2008]
DEBUG - l'offset è -14400000
DEBUG - Creato GMT cal con data [Thu Oct 23 08:09:05 EDT 2008]

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

La parte confusa qui è che Calendar.getTime()ti restituisce un Datenel tuo fuso orario corrente, e inoltre che non esiste alcun metodo per modificare il fuso orario di un calendario e avere anche la data sottostante. A seconda del tipo di parametro utilizzato dal tuo servizio web, potresti semplicemente voler avere l'accordo WS in termini di millisecondi dall'epoca.


11
Non dovresti sottrarre offsetFromUTCinvece di aggiungerlo? Usando il tuo esempio, se 12:09 GMT è 8:09 EDT (che è vero) e l'utente inserisce "12:09 EDT", l'algoritmo dovrebbe produrre "16:09 GMT", a mio parere.
DzinX

29

Grazie a tutti per aver risposto. Dopo un'ulteriore indagine sono arrivato alla risposta giusta. Come menzionato da Skip Head, il TimeStamped che ricevevo dalla mia applicazione veniva adattato al TimeZone dell'utente. Quindi, se l'utente ha inserito le 18:12 (EST), otterrei le 14:12 (GMT). Quello di cui avevo bisogno era un modo per annullare la conversione in modo che l'ora inserita dall'utente fosse l'ora che avevo inviato alla richiesta del WebServer. Ecco come ho ottenuto questo risultato:

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

L'output del codice è: (Utente inserito 5/1/2008 6:12 PM (EST)

Fuso
orario dell'utente corrente: EST Offset corrente dal GMT (in ore): - 4 (Normalmente -5, tranne l'ora legale regolata)
TS da ACP: 2008-05-01 14: 12: 00.0
Data del calendario convertita da TS utilizzando GMT e US_EN Locale : 5/1/08 6:12 PM (GMT)


20

Dici che la data viene utilizzata in connessione con i servizi web, quindi presumo che ad un certo punto sia serializzata in una stringa.

Se questo è il caso, dovresti dare un'occhiata al metodo setTimeZone della classe DateFormat. Ciò determina il fuso orario che verrà utilizzato durante la stampa del timestamp.

Un semplice esempio:

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
Non si è verificato che SDF avesse il proprio fuso orario, chiedendosi perché la modifica di Calendar TimeZone sembra non avere alcun effetto!
Chris.Jenkins

TimeZone.getTimeZone ("UTC") non è valido poiché UTC non è in AvailableIDs () ... dio sa perché
childno͡.de

TimeZone.getTimeZone ("UTC") è disponibile sulla mia macchina. Dipende da JVM?
CodeClimber

2
Fantastica idea con il metodo setTimeZone (). Mi hai risparmiato un sacco di mal di testa, grazie mille!
Bogdan Zurac

1
Proprio quello di cui avevo bisogno! Puoi impostare il fuso orario del server nel formattatore e quindi quando lo converti in formato calendario non devi preoccuparti di nulla. Il metodo migliore in assoluto! per getTimeZone potrebbe essere necessario utilizzare il formato Es: "GMT-4: 00" per ETD
Michael Kern

12

Puoi risolverlo 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"));

attenzione a questo però se stai usando hibernate 4, che non è direttamente compatibile senza dipendenze laterali e configurazione extra. Tuttavia è stato l'approccio più veloce e facile da usare per la terza versione.
Melanzane

8

Sembra che il tuo TimeStamp sia impostato sul fuso orario del sistema di origine.

Questo è deprecato, ma dovrebbe funzionare:

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

Il modo non deprecato è usare

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

ma ciò dovrebbe essere fatto sul lato client, poiché quel sistema sa in quale fuso orario si trova.


7

Metodo per convertire da un fuso orario ad un altro (probabilmente funziona :)).

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

Gli oggetti Data e Timestamp ignorano il fuso orario: rappresentano un certo numero di secondi dall'epoca, senza impegnarsi in una particolare interpretazione di quell'istante come ore e giorni. I fusi orari immettono l'immagine solo in GregorianCalendar (non direttamente necessario per questa attività) e SimpleDateFormat , che richiedono un offset del fuso orario per la conversione tra campi separati e valori Date (o lunghi ).

Il problema dell'OP è proprio all'inizio della sua elaborazione: l'utente inserisce le ore, che sono ambigue, e vengono interpretate nel fuso orario locale, non GMT; a questo punto il valore è "6:12 EST" , che può essere facilmente stampato come "11.12 GMT" o qualsiasi altro fuso orario ma non cambierà mai in "6.12 GMT" .

Non esiste alcun modo per impostare il SimpleDateFormat che analizza "06:12" come "HH: MM" (l'impostazione predefinita per il fuso orario locale) predefinito su UTC; SimpleDateFormat è un po 'troppo intelligente per il suo bene.

Tuttavia, puoi convincere qualsiasi istanza SimpleDateFormat a utilizzare il fuso orario corretto se lo inserisci esplicitamente nell'input: aggiungi semplicemente una stringa fissa al "06:12" ricevuto (e adeguatamente convalidato) per analizzare "06:12 GMT" come "HH: MM z" .

Non è necessario impostare esplicitamente i campi GregorianCalendar o recuperare e utilizzare gli offset del fuso orario e dell'ora legale.

Il vero problema è separare gli input impostati per impostazione predefinita sul fuso orario locale, gli input impostati per impostazione predefinita su UTC e gli input che richiedono davvero un'indicazione del fuso orario esplicita.


4

Qualcosa che ha funzionato per me in passato è stato determinare l'offset (in millisecondi) tra il fuso orario dell'utente e GMT. Una volta ottenuto l'offset, puoi semplicemente aggiungere / sottrarre (a seconda di come sta andando la conversione) per ottenere l'ora appropriata in entrambi i fusi orari. Di solito lo faccio impostando il campo dei millisecondi di un oggetto Calendar, ma sono sicuro che potresti facilmente applicarlo a un oggetto timestamp. Ecco il codice che uso per ottenere l'offset

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

timezoneId è l'id del fuso orario dell'utente (come EST).


9
Utilizzando l'offset grezzo si ignora l'ora legale.
Werner Lehmann,

1
Esatto, questo metodo darà risultati errati per metà anno. La peggiore forma di errore.
Marinaio

1

java.time

L'approccio moderno utilizza le classi java.time che hanno soppiantato le fastidiose classi data-ora legacy in bundle con le prime versioni di Java.

La java.sql.Timestampclasse è una di queste classi legacy. Non è più necessario. Utilizzare invece Instanto altre classi java.time direttamente con il database utilizzando JDBC 4.2 e versioni successive.

La Instantclasse rappresenta un momento sulla sequenza temporale in UTC con una risoluzione di nanosecondi (fino a nove (9) cifre di una frazione decimale).

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

Se devi interagire con un esistente Timestamp, converti immediatamente in java.time tramite i nuovi metodi di conversione aggiunti alle vecchie classi.

Instant instant = myTimestamp.toInstant() ;

Per regolare un altro fuso orario, specificare il fuso orario come ZoneIdoggetto. Specificare un nome proprio fuso orario nel formato continent/region, come ad esempio America/Montreal, Africa/Casablancao Pacific/Auckland. Non utilizzare mai i 3-4 lettera pseudo-zone come ad esempio ESTo ISTcome sono non fusi orari veri e propri, non standardizzati, e nemmeno unico (!).

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

Applica a Instantper produrre un ZonedDateTimeoggetto.

ZonedDateTime zdt = instant.atZone( z ) ;

Per generare una stringa da visualizzare all'utente, cerca Stack Overflow per DateTimeFormattertrovare molte discussioni ed esempi.

La tua domanda riguarda davvero l'andare nella direzione opposta, dall'immissione dei dati dell'utente agli oggetti data-ora. In genere è meglio suddividere l'immissione dei dati in due parti, una data e un'ora del giorno.

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

La tua domanda non è chiara. Vuoi interpretare la data e l'ora inserite dall'utente in UTC? O in un altro fuso orario?

Se si intende UTC, creare un OffsetDateTimecon un offset usando la costante per UTC, ZoneOffset.UTC.

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

Se intendevi un altro fuso orario, combina insieme a un oggetto fuso orario, a ZoneId. Ma quale fuso orario? Potresti rilevare un fuso orario predefinito. Oppure, se critico, devi confermare con l'utente per essere certo della sua intenzione.

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

Per ottenere un oggetto più semplice che sia sempre in UTC per definizione, estrai un file Instant.

Instant instant = odt.toInstant() ;

…o…

Instant instant = zdt.toInstant() ; 

Invia al tuo database.

myPreparedStatement.setObject(  , instant ) ;

Informazioni su java.time

Il framework java.time è integrato in Java 8 e versioni successive. Queste classi soppiantare la vecchia fastidiosi legacy classi data-time come java.util.Date, Calendar, e SimpleDateFormat.

Il progetto Joda-Time , ora in modalità di manutenzione , consiglia la migrazione alle classi java.time .

Per saperne di più, vedere il tutorial Oracle . E cerca Stack Overflow per molti esempi e spiegazioni. La specifica è JSR 310 .

Dove ottenere le classi java.time?

  • Java SE 8 , Java SE 9 e versioni successive
    • Incorporato.
    • Parte dell'API Java standard con un'implementazione in bundle.
    • Java 9 aggiunge alcune funzionalità e correzioni minori.
  • Java SE 6 e Java SE 7
    • Gran parte delle funzionalità java.time sono portate indietro a Java 6 e 7 in ThreeTen-Backport .
  • Android
    • Versioni successive delle implementazioni di bundle Android delle classi java.time.
    • Per precedenza Android, il ThreeTenABP progetto si adatta ThreeTen-Backport (di cui sopra). Vedi Come usare ThreeTenABP… .

Il progetto ThreeTen-Extra estende java.time con classi aggiuntive. Questo progetto è un banco di prova per possibili future aggiunte a java.time. Si possono trovare alcune classi utili, come per esempio Interval, YearWeek, YearQuartere altro .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.