Jak obsługiwać TimeZones kalendarza za pomocą Java?

92

Mam wartość datownika pochodzącą z mojej aplikacji. Użytkownik może znajdować się w dowolnej lokalnej strefie czasowej.

Ponieważ ta data jest używana dla usługi sieciowej, która zakłada, że ​​podany czas jest zawsze w GMT, muszę przekonwertować parametr użytkownika z powiedzmy (EST) na (GMT). Oto kicker: użytkownik jest nieświadomy swojego TZ. Wpisuje datę utworzenia, którą chce wysłać do WS, więc potrzebuję:

Użytkownik wprowadza: 5/1/2008 6:12 PM (EST)
Parametr do WS musi być : 5/1/2008 6:12 PM (GMT)

Wiem, że znaczniki czasu powinny być zawsze domyślnie ustawione w GMT, ale podczas wysyłania parametru, mimo że utworzyłem swój kalendarz z TS (który powinien być w GMT), godziny są zawsze wyłączone, chyba że użytkownik jest w GMT. czego mi brakuje?

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

W przypadku poprzedniego kodu w rezultacie otrzymuję (krótki format ułatwiający czytanie):

[1 maja 2008 r. 23:12]

Jorge Valois
źródło
2
Dlaczego zmieniasz tylko strefę czasową i nie konwertujesz wraz z nią daty / czasu?
Spencer Kormos
1
To prawdziwy ból, aby wziąć datę Java w jednej strefie czasowej i uzyskać datę w innej strefie czasowej. IE, weź 17:00 czasu EDT i otrzymaj 17:00 czasu PDT.
mtyson

Odpowiedzi:

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

Oto wynik, jeśli przekażę bieżący czas („12:09:05 EDT” od Calendar.getInstance()) w:

DEBUG - kalendarz wejściowy ma datę [Thu Oct 23 12:09:05 EDT 2008]
DEBUG - przesunięcie wynosi -14400000
DEBUG - Utworzono kalibrację GMT z datą [Thu Oct 23 08:09:05 EDT 2008]

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

Mylące jest to, że Calendar.getTime()zwraca ci to Datew bieżącej strefie czasowej, a także, że nie ma metody na modyfikację strefy czasowej kalendarza i wyrzucenie podstawowej daty. W zależności od typu parametru pobieranego przez twoją usługę internetową, możesz po prostu chcieć, aby umowa WS była wyrażona w milisekundach od epoki.

matowe b
źródło
11
Nie powinieneś odejmować offsetFromUTCzamiast go dodawać? Używając twojego przykładu, jeśli 12:09 GMT to 8:09 EDT (co jest prawdą), a użytkownik wprowadzi „12:09 EDT”, algorytm powinien wypisać „16:09 GMT”, moim zdaniem.
DzinX
29

Dziękuję wszystkim za odpowiedź. Po dalszym dochodzeniu znalazłem właściwą odpowiedź. Jak wspomniał Skip Head, znacznik czasu, który otrzymywałem z mojej aplikacji, był dostosowywany do strefy czasowej użytkownika. Więc jeśli użytkownik wprowadzi 18:12 (EST), otrzymam 14:12 (GMT). Potrzebowałem sposobu na cofnięcie konwersji tak, aby czas wprowadzony przez użytkownika był czasem wysłania do żądania WebServer. Oto jak to osiągnąłem:

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

Kod wyjściowy to: (Użytkownik wprowadzony 01.05.2008 o 18:12 (EST)

Strefa czasowa bieżącego użytkownika: EST
Bieżące przesunięcie względem GMT (w godzinach): - 4 (zwykle -5, z wyjątkiem korekty na czas letni)
TS z ACP: 2008-05-01 14: 12: 00.0
Data kalendarza przekonwertowana z TS przy użyciu GMT i US_EN Locale : 01.05.08 18:12 (GMT)

Jorge Valois
źródło
20

Mówisz, że data jest używana w połączeniu z usługami internetowymi, więc zakładam, że w pewnym momencie jest ona serializowana do łańcucha.

W takim przypadku należy przyjrzeć się metodzie setTimeZone klasy DateFormat. Określa, która strefa czasowa zostanie użyta podczas drukowania znacznika czasu.

Prosty przykład:

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
źródło
4
Nie uruchomiono ponownie SDF Gdyby była to własna strefa czasowa, zastanawiałem się, dlaczego zmiana kalendarza TimeZone wydaje się nie mieć żadnego efektu!
Chris.Jenkins
TimeZone.getTimeZone („UTC”) jest nieprawidłowa, ponieważ UTC nie znajduje się w AvailableIDs () ... Bóg wie dlaczego
childno͡.de
TimeZone.getTimeZone („UTC”) jest dostępna na moim komputerze. Czy jest to zależne od JVM?
CodeClimber,
2
Fantastyczny pomysł z metodą setTimeZone (). Uratowałeś mi wiele bólów głowy, wielkie dzięki!
Bogdan Zurac
1
Dokładnie to, czego potrzebowałem! Możesz ustawić strefę czasową serwera w programie formatującym, a po przekonwertowaniu go na format kalendarza nie masz się czym martwić. Zdecydowanie najlepsza metoda! w przypadku getTimeZone może być konieczne użycie formatu Np .: „GMT-4: 00” dla ETD
Michael Kern
12

Możesz go rozwiązać za pomocą 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
źródło
uważaj jednak na to, jeśli używasz hibernacji 4, która nie jest bezpośrednio kompatybilna bez zależności bocznych i dodatkowej konfiguracji. Jednak było to najszybsze i najłatwiejsze w użyciu podejście dla trzeciej wersji.
Bakłażan
8

Wygląda na to, że Twój znacznik czasu jest ustawiany na strefę czasową systemu, z którego pochodzi.

To jest przestarzałe, ale powinno działać:

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

Nieaktualnym sposobem jest użycie

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

ale trzeba by to zrobić po stronie klienta, ponieważ ten system wie, w jakiej strefie czasowej się znajduje.

Skip Head
źródło
7

Metoda konwersji z jednej strefy czasowej na inną (prawdopodobnie działa :)).

/**
 * 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
źródło
6

Obiekty Date i Timestamp nie zwracają uwagi na strefę czasową: reprezentują określoną liczbę sekund od epoki, bez konieczności określonej interpretacji tej chwili jako godzin i dni. Strefy czasowe wprowadzają obraz tylko w GregorianCalendar (nie jest to bezpośrednio potrzebne do tego zadania) i SimpleDateFormat , które wymagają przesunięcia strefy czasowej, aby przekonwertować oddzielne pola i wartości typu Date (lub długie ).

Problem OP jest już na początku jego przetwarzania: użytkownik wprowadza godziny, które są niejednoznaczne i są interpretowane w lokalnej strefie czasowej innej niż GMT; w tym momencie wartość to „6:12 EST” , którą można łatwo wydrukować jako „11.12 GMT” lub inną strefę czasową, ale nigdy nie zmieni się na „6.12 GMT” .

Nie ma sposobu, aby SimpleDateFormat analizował „06:12” jako „HH: MM” (domyślnie lokalna strefa czasowa) jako domyślny czas UTC; SimpleDateFormat jest trochę zbyt inteligentny dla własnego dobra.

Możesz jednak przekonać każdego instancję SimpleDateFormat do korzystania z właściwej strefy czasowej, jeśli umieścisz ją wyraźnie w danych wejściowych: po prostu dodaj ustalony ciąg do odebranego (i odpowiednio zweryfikowanego) „06:12”, aby przeanalizować „06:12 GMT” jako „HH: MM z” .

Nie ma potrzeby bezpośredniego ustawiania pól GregorianCalendar ani pobierania i używania stref czasowych i przesunięć czasu letniego.

Prawdziwym problemem jest segregacja wejść, które domyślnie dotyczą lokalnej strefy czasowej, wejść domyślnych dla UTC i wejść, które naprawdę wymagają wyraźnego wskazania strefy czasowej.

user412090
źródło
4

Coś, co działało dla mnie w przeszłości, polegało na określeniu przesunięcia (w milisekundach) między strefą czasową użytkownika a GMT. Gdy już masz przesunięcie, możesz po prostu dodać / odjąć (w zależności od tego, w jaki sposób przebiega konwersja), aby uzyskać odpowiedni czas w obu strefach czasowych. Zwykle robiłbym to, ustawiając pole milisekund obiektu kalendarza, ale jestem pewien, że można go łatwo zastosować do obiektu znacznika czasu. Oto kod, którego używam, aby uzyskać przesunięcie

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

timezoneId to identyfikator strefy czasowej użytkownika (na przykład EST).

Adam
źródło
9
Używając surowego przesunięcia, ignorujesz czas letni.
Werner Lehmann
1
Dokładnie, ta metoda da nieprawidłowe wyniki przez pół roku. Najgorsza forma błędu.
Danubian Sailor
1

java.time

Nowoczesne podejście wykorzystuje klasy java.time , które wyparły kłopotliwe starsze klasy daty i czasu, zawarte w najwcześniejszych wersjach Javy.

java.sql.TimestampKlasa jest jedną z tych klas starszych. Nie jest już potrzebne. Zamiast tego użyj Instantlub innych klas java.time bezpośrednio z bazą danych przy użyciu JDBC 4.2 i nowszych.

InstantKlasa reprezentuje moment na osi czasu w UTC z rozdzielczością nanosekund (do dziewięciu (9) cyfr ułamka dziesiętnego).

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

Jeśli musisz współdziałać z istniejącą Timestamp, natychmiast przekonwertuj ją na java.time za pomocą nowych metod konwersji dodanych do starych klas.

Instant instant = myTimestamp.toInstant() ;

Aby dostosować się do innej strefy czasowej, określ strefę czasową jako ZoneIdobiekt. Określ prawidłową nazwę strefy czasowej w formacie continent/region, takie jak America/Montreal, Africa/Casablancalub Pacific/Auckland. Nigdy nie używaj 3-4-literowych pseudostref, takich jak ESTlub ISTponieważ nie są one prawdziwymi strefami czasowymi, nie znormalizowane, a nawet nie są unikalne (!).

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

Zastosuj do, Instantaby utworzyć ZonedDateTimeobiekt.

ZonedDateTime zdt = instant.atZone( z ) ;

Aby wygenerować ciąg do wyświetlenia użytkownikowi, przeszukaj Stack Overflow, DateTimeFormatteraby znaleźć wiele dyskusji i przykładów.

Twoje pytanie tak naprawdę dotyczy pójścia w innym kierunku, od wprowadzania danych użytkownika do obiektów daty i czasu. Ogólnie najlepiej jest podzielić wprowadzanie danych na dwie części, datę i godzinę.

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

Twoje pytanie nie jest jasne. Czy chcesz interpretować datę i godzinę wprowadzoną przez użytkownika jako czas UTC? Albo w innej strefie czasowej?

Jeśli masz na myśli UTC, stworzyć OffsetDateTimez przesunięciem za pomocą stałej za UTC ZoneOffset.UTC.

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

Jeśli masz na myśli inną strefę czasową, połącz razem z obiektem strefy czasowej, a ZoneId. Ale która strefa czasowa? Możesz wykryć domyślną strefę czasową. Lub, jeśli jest to krytyczne, musisz potwierdzić z użytkownikiem, aby mieć pewność co do jego zamiaru.

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

Aby uzyskać prostszy obiekt, który z definicji zawsze znajduje się w UTC, wyodrębnij plik Instant.

Instant instant = odt.toInstant() ;

…lub…

Instant instant = zdt.toInstant() ; 

Wyślij do swojej bazy danych.

myPreparedStatement.setObject( … , instant ) ;

Informacje o java.time

Struktura java.time jest wbudowana w Javę 8 i nowsze. Klasy te kłopotliwe zastąpić stary starszych klas Date-Time, takich jak java.util.Date, Calendar, iSimpleDateFormat .

Projekt Joda-Time , obecnie w trybie konserwacji , zaleca migrację do klas java.time .

Aby dowiedzieć się więcej, zobacz samouczek Oracle . I przeszukaj Stack Overflow, aby znaleźć wiele przykładów i wyjaśnień. Specyfikacja to JSR 310 .

Skąd wziąć klasy java.time?

  • Java SE 8 , Java SE 9 i nowsze
    • Wbudowany.
    • Część standardowego interfejsu API języka Java z dołączoną implementacją.
    • Java 9 dodaje kilka drobnych funkcji i poprawek.
  • Java SE 6 i Java SE 7
    • Wiele funkcji java.time jest przenoszonych z powrotem do Java 6 i 7 w ThreeTen-Backport .
  • Android
    • Późniejsze wersje implementacji klas java.time w pakietach Androida.
    • Na wcześniejszym Androidzie ThreeTenABP projekt dostosowuje ThreeTen-backportu (wymienione powyżej). Zobacz Jak używać ThreeTenABP… .

Projekt ThreeTen-Extra rozszerza java.time o dodatkowe klasy. Ten projekt jest poligonem doświadczalnym dla ewentualnych przyszłych dodatków do java.time. Można znaleźć kilka przydatnych klas tutaj takie jak Interval, YearWeek, YearQuarter, i więcej .

Basil Bourque
źródło