Wie gehe ich mit Kalender-TimeZones mit Java um?

92

Ich habe einen Zeitstempelwert, der aus meiner Anwendung stammt. Der Benutzer kann sich in einer bestimmten lokalen Zeitzone befinden.

Da dieses Datum für einen WebService verwendet wird, der davon ausgeht, dass die angegebene Zeit immer in GMT angegeben ist, muss der Parameter des Benutzers von say (EST) in (GMT) konvertiert werden. Hier ist der Kicker: Der Benutzer ist sich seiner TZ nicht bewusst. Er gibt das Erstellungsdatum ein, das er an das WS senden möchte. Ich brauche also Folgendes:

Benutzer gibt ein: 01.05.2008 18:12 Uhr (EST)
Der Parameter für das WS muss sein : 01.05.2008 18:12 Uhr (GMT)

Ich weiß, dass TimeStamps standardmäßig immer in GMT sein sollen, aber beim Senden des Parameters sind die Stunden immer ausgeschaltet, obwohl ich meinen Kalender aus dem TS erstellt habe (der in GMT sein soll), es sei denn, der Benutzer ist in GMT. Was vermisse ich?

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

Mit dem vorherigen Code erhalte ich Folgendes (Kurzformat zum einfachen Lesen):

[1. Mai 2008, 23:12 Uhr]

Jorge Valois
quelle
2
Warum ändern Sie nur die Zeitzone und konvertieren nicht das Datum / die Uhrzeit mit?
Spencer Kormos
1
Es ist ein echtes Problem, ein Java-Datum in einer Zeitzone zu nehmen und dieses Datum in einer anderen Zeitzone abzurufen. IE, nehmen Sie 5PM EDT und erhalten Sie 5PM PDT.
Mtyson

Antworten:

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

Hier ist die Ausgabe, wenn ich die aktuelle Zeit ("12:09:05 EDT" von Calendar.getInstance()) in übergebe:

DEBUG - Eingabekalender hat Datum [Do 23.10. 12:09:05 EDT 2008]
DEBUG - Offset ist -14400000
DEBUG - Erstellt GMT cal mit Datum [Do 23.10. 08:09:05 EDT 2008]

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

Der verwirrende Teil hier ist, dass Calendar.getTime()Sie eine Datein Ihrer aktuellen Zeitzone zurückgeben und dass es keine Methode gibt, die Zeitzone eines Kalenders zu ändern und das zugrunde liegende Datum ebenfalls zu rollen. Abhängig davon, welche Art von Parameter Ihr Webdienst verwendet, möchten Sie möglicherweise nur den WS-Deal in Millisekunden ab der Epoche.

matt b
quelle
11
Sollten Sie nicht subtrahieren, offsetFromUTCanstatt es zu addieren? Wenn in Ihrem Beispiel 12:09 GMT 8:09 EDT ist (was wahr ist) und der Benutzer "12:09 EDT" eingibt, sollte der Algorithmus meiner Meinung nach "16:09 GMT" ausgeben.
DzinX
29

Vielen Dank für Ihre Antwort. Nach einer weiteren Untersuchung kam ich zur richtigen Antwort. Wie von Skip Head erwähnt, wurde der Zeitstempel, den ich von meiner Anwendung erhielt, an die Zeitzone des Benutzers angepasst. Wenn der Benutzer also 18:12 Uhr (EST) eingibt, erhalte ich 14:12 Uhr (GMT). Was ich brauchte, war eine Möglichkeit, die Konvertierung rückgängig zu machen, sodass die vom Benutzer eingegebene Zeit die Zeit ist, die ich an die WebServer-Anforderung gesendet habe. So habe ich das erreicht:

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

Die Ausgabe des Codes lautet: (Benutzer eingegeben am 01.05.2008, 18:12 Uhr (EST)


Zeitzone des aktuellen Benutzers: EST Aktueller Versatz von GMT (in Stunden): - 4 (Normalerweise -5, außer DST angepasst)
TS von ACP: 2008-05-01 14: 12: 00.0
Kalenderdatum von TS mit GMT und US_EN-Gebietsschema konvertiert : 01.05.08 18:12 Uhr (GMT)

Jorge Valois
quelle
20

Sie sagen, dass das Datum in Verbindung mit Webdiensten verwendet wird, also gehe ich davon aus, dass es irgendwann zu einer Zeichenfolge serialisiert wird.

In diesem Fall sollten Sie sich die setTimeZone-Methode der DateFormat-Klasse ansehen. Dies bestimmt, welche Zeitzone beim Drucken des Zeitstempels verwendet wird.

Ein einfaches Beispiel:

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
quelle
4
SDF wurde nicht neu gestartet Hatte eine eigene Zeitzone und fragte sich, warum das Ändern der Kalender-Zeitzone keine Auswirkungen zu haben scheint!
Chris.Jenkins
TimeZone.getTimeZone ("UTC") ist ungültig, da UTC nicht in AvailableIDs () enthalten ist ... Gott weiß warum
childno͡.de
TimeZone.getTimeZone ("UTC") ist auf meinem Computer verfügbar. Ist es JVM-abhängig?
CodeClimber
2
Tolle Idee mit der Methode setTimeZone (). Du hast mir viele Kopfschmerzen erspart, vielen Dank!
Bogdan Zurac
1
Genau das, was ich brauchte! Sie können die Serverzeitzone im Formatierer festlegen. Wenn Sie sie dann in das Kalenderformat konvertieren, müssen Sie sich keine Sorgen machen. Mit Abstand beste Methode! Für getTimeZone müssen Sie möglicherweise das Format Bsp.: "GMT-4: 00" für ETD verwenden
Michael Kern
12

Sie können es mit Joda Time lösen :

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
quelle
Beachten Sie dies jedoch, wenn Sie den Ruhezustand 4 verwenden, der ohne Seitenabhängigkeit und zusätzliche Konfiguration nicht direkt kompatibel ist. Es war jedoch der schnellste und am einfachsten zu verwendende Ansatz für die 3. Version.
Aubergine
8

Es sieht so aus, als würde Ihr TimeStamp auf die Zeitzone des Ursprungssystems eingestellt.

Dies ist veraltet, aber es sollte funktionieren:

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

Der nicht veraltete Weg ist zu verwenden

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

Dies müsste jedoch auf der Client-Seite erfolgen, da dieses System weiß, in welcher Zeitzone es sich befindet.

Kopf überspringen
quelle
7

Methode zum Konvertieren von einer Zeitzone in eine andere (wahrscheinlich funktioniert es :)).

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

Datums- und Zeitstempelobjekte sind zeitzonenunabhängig: Sie repräsentieren eine bestimmte Anzahl von Sekunden seit der Epoche, ohne sich auf eine bestimmte Interpretation dieses Augenblicks als Stunden und Tage festzulegen. Zeitzonen geben das Bild nur in GregorianCalendar (für diese Aufgabe nicht direkt erforderlich) und SimpleDateFormat ein , die einen Zeitzonenversatz benötigen, um zwischen separaten Feldern und Datumswerten (oder langen Werten) zu konvertieren .

Das Problem des OP liegt gleich zu Beginn seiner Verarbeitung: Der Benutzer gibt Stunden ein, die nicht eindeutig sind und in der lokalen Zeitzone ohne GMT interpretiert werden. Zu diesem Zeitpunkt ist der Wert "6:12 EST" , der leicht als "11.12 GMT" oder eine andere Zeitzone gedruckt werden kann, sich jedoch niemals in "6.12 GMT" ändert .

Es gibt keine Möglichkeit, das SimpleDateFormat , das "06:12" als "HH: MM" analysiert (standardmäßig die lokale Zeitzone), stattdessen auf UTC zu setzen. SimpleDateFormat ist ein bisschen zu schlau für sich.

Sie können jedoch jede SimpleDateFormat- Instanz davon überzeugen , die richtige Zeitzone zu verwenden, wenn Sie sie explizit in die Eingabe einfügen: Fügen Sie einfach eine feste Zeichenfolge an die empfangene (und ausreichend validierte) "06:12" an, um "06:12 GMT" als zu analysieren "HH: MM z" .

Es ist nicht erforderlich, GregorianCalendar- Felder explizit festzulegen oder Zeitzonen- und Sommerzeit-Offsets abzurufen und zu verwenden.

Das eigentliche Problem besteht darin, Eingaben zu trennen, die standardmäßig auf die lokale Zeitzone eingestellt sind, Eingaben, die standardmäßig auf UTC eingestellt sind, und Eingaben, für die eine explizite Zeitzonenangabe erforderlich ist.

user412090
quelle
4

In der Vergangenheit hat es für mich funktioniert, den Versatz (in Millisekunden) zwischen der Zeitzone des Benutzers und GMT zu bestimmen. Sobald Sie den Versatz haben, können Sie einfach addieren / subtrahieren (abhängig davon, in welche Richtung die Konvertierung verläuft), um die entsprechende Zeit in jeder Zeitzone zu erhalten. Normalerweise würde ich dies erreichen, indem ich das Millisekundenfeld eines Kalenderobjekts einstelle, aber ich bin sicher, dass Sie es leicht auf ein Zeitstempelobjekt anwenden können. Hier ist der Code, mit dem ich den Offset erhalte

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

timezoneId ist die ID der Zeitzone des Benutzers (z. B. EST).

Adam
quelle
9
Mit dem Rohoffset ignorieren Sie die Sommerzeit.
Werner Lehmann
1
Genau diese Methode liefert ein halbes Jahr lang falsche Ergebnisse. Die schlimmste Form des Fehlers.
Danubian Sailor
1

java.time

Der moderne Ansatz verwendet die java.time- Klassen, die die problematischen älteren Datums- / Zeitklassen ersetzen, die mit den frühesten Versionen von Java gebündelt sind.

Die java.sql.TimestampKlasse ist eine dieser Legacy-Klassen. Nicht mehr gebraucht. Verwenden Sie stattdessen Instantoder andere java.time-Klassen direkt mit Ihrer Datenbank unter Verwendung von JDBC 4.2 und höher.

Die InstantKlasse repräsentiert einen Moment auf der Zeitachse in UTC mit einer Auflösung von Nanosekunden (bis zu neun (9) Stellen eines Dezimalbruchs).

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

Wenn Sie mit einer vorhandenen zusammenarbeiten müssen Timestamp, konvertieren Sie diese sofort in java.time über die neuen Konvertierungsmethoden, die den alten Klassen hinzugefügt wurden.

Instant instant = myTimestamp.toInstant() ;

Geben Sie zum Anpassen in eine andere Zeitzone die Zeitzone als ZoneIdObjekt an. Geben Sie einen richtigen Zeitzonennamen im Format continent/region, wie zum Beispiel America/Montreal, Africa/Casablancaoder Pacific/Auckland. Verwenden Sie niemals Pseudozonen mit 3-4 Buchstaben, wie z. B. ESToder ISTweil sie keine echten Zeitzonen sind, nicht standardisiert und nicht einmal eindeutig (!).

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

Auf das anwenden, Instantum ein ZonedDateTimeObjekt zu erzeugen .

ZonedDateTime zdt = instant.atZone( z ) ;

Um eine Zeichenfolge für die Anzeige für den Benutzer zu generieren, durchsuchen Sie den Stapelüberlauf DateTimeFormatternach vielen Diskussionen und Beispielen.

Bei Ihrer Frage geht es wirklich darum, in die andere Richtung zu gehen, von der Eingabe von Benutzerdaten bis zu den Datums- / Uhrzeitobjekten. Im Allgemeinen ist es am besten, Ihre Dateneingabe in zwei Teile zu unterteilen, ein Datum und eine Uhrzeit.

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

Ihre Frage ist nicht klar. Möchten Sie das Datum und die Uhrzeit interpretieren, die der Benutzer eingegeben hat, um in UTC zu sein? Oder in einer anderen Zeitzone?

Wenn Sie UTC gemeint haben, erstellen Sie eine OffsetDateTimemit einem Offset unter Verwendung der Konstante für UTC , ZoneOffset.UTC.

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

Wenn Sie eine andere Zeitzone gemeint haben, kombinieren Sie diese zusammen mit einem Zeitzonenobjekt, a ZoneId. Aber welche Zeitzone? Möglicherweise erkennen Sie eine Standardzeitzone. Wenn dies kritisch ist, müssen Sie dies dem Benutzer bestätigen, um sicherzugehen, dass er beabsichtigt ist.

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

Um ein einfacheres Objekt zu erhalten, das sich per Definition immer in UTC befindet, extrahieren Sie ein Instant.

Instant instant = odt.toInstant() ;

…oder…

Instant instant = zdt.toInstant() ; 

An Ihre Datenbank senden.

myPreparedStatement.setObject(  , instant ) ;

Über java.time

Das java.time- Framework ist in Java 8 und höher integriert. Diese Klassen verdrängen die lästigen alten Legacy - Datum-Zeit - Klassen wie java.util.Date, Calendar, & SimpleDateFormat.

Das Joda-Time- Projekt, das sich jetzt im Wartungsmodus befindet , empfiehlt die Migration zu den Klassen java.time .

Weitere Informationen finden Sie im Oracle-Lernprogramm . Suchen Sie im Stapelüberlauf nach vielen Beispielen und Erklärungen. Die Spezifikation ist JSR 310 .

Woher bekomme ich die java.time-Klassen?

Das ThreeTen-Extra- Projekt erweitert java.time um zusätzliche Klassen. Dieses Projekt ist ein Testfeld für mögliche zukünftige Ergänzungen von java.time. Sie können einige nützliche Klassen hier wie finden Interval, YearWeek, YearQuarter, und mehr .

Basil Bourque
quelle