Bagaimana menangani zona waktu kalender menggunakan Java?

92

Saya memiliki nilai Stempel waktu yang berasal dari aplikasi saya. Pengguna dapat berada di TimeZone lokal mana pun.

Karena tanggal ini digunakan untuk WebService yang mengasumsikan waktu yang diberikan selalu dalam GMT, saya perlu mengonversi parameter pengguna dari say (EST) menjadi (GMT). Inilah kickernya: Pengguna tidak menyadari TZ-nya. Dia memasukkan tanggal pembuatan yang ingin dia kirim ke WS, jadi yang saya butuhkan adalah:

Masukkan pengguna: 5/1/2008 6:12 PM (EST)
Parameter ke WS harus : 5/1/2008 6:12 PM (GMT)

Saya tahu Stempel Waktu seharusnya selalu dalam GMT secara default, tetapi ketika mengirim parameter, meskipun saya membuat Kalender saya dari TS (yang seharusnya dalam GMT), jam selalu tidak aktif kecuali pengguna dalam GMT. Apa yang saya lewatkan?

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

Dengan Kode sebelumnya, inilah yang saya dapatkan sebagai hasilnya (Format Singkat agar mudah dibaca):

[1 Mei 2008 11:12]

Jorge Valois
sumber
2
Mengapa Anda hanya mengubah zona waktu dan tidak mengubah tanggal / waktu bersamanya?
Spencer Kormos
1
Its sakit nyata untuk mengambil tanggal Java yang ada di satu zona waktu, dan mendapatkan bahwa tanggal di zona waktu yang lain. Yaitu, ambil 5PM EDT dan dapatkan 5PM PDT.
mtyson

Jawaban:

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

Berikut outputnya jika saya melewatkan waktu saat ini ("12:09:05 EDT" dari Calendar.getInstance()) di:

DEBUG - kalender input bertanggal [Kam 23 Okt 12:09:05 EDT 2008]
DEBUG - offset -14400000
DEBUG - Dibuat kal GMT dengan tanggal [Kam 23 Okt 08:09:05 EDT 2008]

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

Bagian yang membingungkan di sini adalah yang Calendar.getTime()mengembalikan Anda Datepada zona waktu Anda saat ini, dan juga tidak ada metode untuk mengubah zona waktu kalender dan juga tanggal yang mendasarinya. Bergantung pada jenis parameter apa yang diambil layanan web Anda, Anda mungkin hanya ingin memiliki kesepakatan WS dalam milidetik dari epoch.

matt b
sumber
11
Bukankah seharusnya Anda mengurangi offsetFromUTCdaripada menambahkannya? Menggunakan contoh Anda, jika 12:09 GMT adalah 8:09 EDT (yang benar), dan pengguna memasukkan "12:09 EDT", algoritme harus menghasilkan "16:09 GMT", menurut pendapat saya.
DzinX
29

Terima kasih atas tanggapannya. Setelah penyelidikan lebih lanjut, saya mendapat jawaban yang benar. Seperti yang disebutkan oleh Skip Head, TimeStamped yang saya dapatkan dari aplikasi saya sedang disesuaikan dengan TimeZone pengguna. Jadi jika Pengguna memasukkan 6:12 PM (EST) saya akan mendapatkan 2:12 PM (GMT). Apa yang saya butuhkan adalah cara untuk membatalkan konversi sehingga waktu yang dimasukkan oleh pengguna adalah waktu yang saya kirimkan ke permintaan WebServer. Inilah cara saya mencapai ini:

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

Output kode adalah: (Pengguna memasukkan 5/1/2008 6:12 PM (EST)

Zona Waktu Pengguna Saat Ini: EST
Offset Saat Ini dari GMT (dalam jam): - 4 (Biasanya -5, kecuali disesuaikan DST)
TS dari ACP: 2008-05-01 14: 12: 00.0
Tanggal Kalender dikonversi dari TS menggunakan GMT dan US_EN Lokal : 1/5/08 18:12 (GMT)

Jorge Valois
sumber
20

Anda mengatakan bahwa tanggal digunakan sehubungan dengan layanan web, jadi saya berasumsi bahwa itu diserialkan menjadi string di beberapa titik.

Jika demikian, Anda harus melihat metode setTimeZone dari kelas DateFormat. Ini menentukan zona waktu mana yang akan digunakan saat mencetak cap waktu.

Contoh sederhana:

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
sumber
4
Tidak relise SDF Memiliki zona waktu sendiri, bertanya-tanya mengapa mengubah TimeZone Kalender tampaknya tidak berpengaruh!
Chris.Jenkins
TimeZone.getTimeZone ("UTC") tidak valid karena UTC tidak ada dalam AvailableIDs () ... entah mengapa
childno͡.de
TimeZone.getTimeZone ("UTC") tersedia di komputer saya. Apakah itu tergantung JVM?
CodeClimber
2
Ide luar biasa dengan metode setTimeZone (). Anda menyelamatkan saya dari banyak sakit kepala, terima kasih banyak!
Bogdan Zurac
1
Hanya yang saya butuhkan! Anda dapat mengatur zona waktu server di pemformat dan kemudian saat Anda mengonversinya ke format kalender Anda tidak perlu khawatir. Metode terbaik sejauh ini! untuk getTimeZone Anda mungkin perlu menggunakan format Contoh: "GMT-4: 00" untuk ETD
Michael Kern
12

Anda bisa mengatasinya dengan Joda Time :

Date utcDate = new Date(timezoneFrom.convertLocalToUTC(date.getTime(), false));
Date localDate = new Date(timezoneTo.convertUTCToLocal(utcDate.getTime()));

Jawa 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
sumber
Namun berhati-hatilah untuk ini jika Anda menggunakan hibernate 4, yang tidak langsung kompatibel tanpa ketergantungan samping dan konfigurasi tambahan. Namun itu adalah pendekatan tercepat dan termudah untuk digunakan untuk versi ke-3.
Aubergine
8

Sepertinya Stempel Waktu Anda disetel ke zona waktu dari sistem asal.

Ini sudah usang, tetapi seharusnya berfungsi:

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

Cara yang tidak digunakan lagi adalah dengan menggunakan

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

tetapi itu perlu dilakukan di sisi klien, karena sistem tersebut mengetahui zona waktunya.

Lewati Kepala
sumber
7

Metode untuk mengonversi dari satu zona waktu ke zona waktu lainnya (mungkin berhasil :)).

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

Objek Tanggal dan Stempel Waktu tidak menyadari zona waktu: objek tersebut mewakili jumlah detik tertentu sejak zaman, tanpa melakukan interpretasi tertentu saat itu sebagai jam dan hari. Zona waktu memasukkan gambar hanya di GregorianCalendar (tidak diperlukan secara langsung untuk tugas ini) dan SimpleDateFormat , yang memerlukan offset zona waktu untuk mengonversi antara bidang terpisah dan nilai Tanggal (atau panjang ).

Masalah OP ada tepat di awal pemrosesannya: jam input pengguna, yang ambigu, dan ditafsirkan dalam zona waktu lokal non-GMT; pada titik ini nilainya adalah "6:12 EST" , yang dapat dengan mudah dicetak sebagai "11.12 GMT" atau zona waktu lainnya tetapi tidak akan pernah berubah menjadi "6.12 GMT" .

Tidak ada cara untuk membuat SimpleDateFormat yang mengurai "06:12" sebagai "HH: MM" (default ke zona waktu lokal) sebagai default ke UTC; SimpleDateFormat agak terlalu pintar untuk kebaikannya sendiri.

Namun, Anda dapat meyakinkan setiap instance SimpleDateFormat untuk menggunakan zona waktu yang benar jika Anda meletakkannya secara eksplisit di input: cukup tambahkan string tetap ke "06:12" yang diterima (dan divalidasi secara memadai) untuk mengurai "06:12 GMT" sebagai "HH: MM z" .

Tidak perlu pengaturan eksplisit bidang Kalender Gregorian atau mengambil dan menggunakan offset zona waktu dan waktu musim panas.

Masalah sebenarnya adalah memisahkan input yang default ke zona waktu lokal, input yang default ke UTC, dan input yang benar-benar membutuhkan indikasi zona waktu yang eksplisit.

pengguna412090
sumber
4

Sesuatu yang berhasil bagi saya di masa lalu adalah menentukan offset (dalam milidetik) antara zona waktu pengguna dan GMT. Setelah Anda memiliki offset, Anda dapat menambahkan / mengurangi (tergantung ke arah mana konversi akan) untuk mendapatkan waktu yang sesuai di kedua zona waktu. Saya biasanya akan melakukannya dengan menyetel bidang milidetik dari objek Kalender, tetapi saya yakin Anda dapat dengan mudah menerapkannya ke objek stempel waktu. Berikut kode yang saya gunakan untuk mendapatkan offset

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

timezoneId adalah id zona waktu pengguna (seperti EST).

Adam
sumber
9
Menggunakan offset mentah, Anda mengabaikan DST.
Werner Lehmann
1
Tepatnya, cara ini akan memberikan hasil yang salah selama setengah tahun. Bentuk kesalahan terburuk.
Danubian Sailor
1

java.time

Pendekatan modern menggunakan kelas java.time yang menggantikan kelas tanggal-waktu lama yang merepotkan yang digabungkan dengan versi Java yang paling awal.

The java.sql.Timestampkelas adalah salah satu kelas warisan. Tidak lagi dibutuhkan. Sebagai gantinya gunakan Instantatau kelas java.time lainnya secara langsung dengan database Anda menggunakan JDBC 4.2 dan yang lebih baru.

The Instantkelas merupakan saat di timeline di UTC dengan resolusi nanodetik (hingga sembilan (9) angka dari pecahan desimal).

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

Jika Anda harus beroperasi dengan yang sudah ada Timestamp, segera ubah menjadi java.time melalui metode konversi baru yang ditambahkan ke kelas lama.

Instant instant = myTimestamp.toInstant() ;

Untuk menyesuaikan dengan zona waktu lain, tentukan zona waktu sebagai ZoneIdobjek. Tentukan nama zona waktu yang tepat dalam format continent/region, seperti America/Montreal,Africa/Casablanca , atau Pacific/Auckland. Jangan pernah menggunakan 3-4 huruf pseudo-zone seperti ESTatau ISTkarena itu bukan zona waktu yang sebenarnya, tidak terstandarisasi, dan bahkan tidak unik (!).

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

Terapkan ke Instantuntuk menghasilkan ZonedDateTimeobjek.

ZonedDateTime zdt = instant.atZone( z ) ;

Untuk menghasilkan string untuk ditampilkan kepada pengguna, telusuri Stack Overflow untuk DateTimeFormattermenemukan banyak diskusi dan contoh.

Pertanyaan Anda benar-benar tentang pergi ke arah lain, dari entri data pengguna ke objek tanggal-waktu. Umumnya paling baik untuk memecah entri data Anda menjadi dua bagian, tanggal dan waktu.

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

Pertanyaanmu tidak jelas. Apakah Anda ingin mengartikan tanggal dan waktu yang dimasukkan oleh pengguna sebagai UTC? Atau di zona waktu lain?

Jika yang Anda maksud adalah UTC, buat file OffsetDateTime dengan offset menggunakan konstanta untuk UTC ZoneOffset.UTC,.

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

Jika yang Anda maksud zona waktu lain, gabungkan dengan objek zona waktu, a ZoneId . Tapi zona waktu yang mana? Anda mungkin mendeteksi zona waktu default. Atau, jika penting, Anda harus mengonfirmasi dengan pengguna untuk memastikan niat mereka.

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

Untuk mendapatkan objek yang lebih sederhana yang selalu dalam UTC menurut definisi, ekstrak file Instant .

Instant instant = odt.toInstant() ;

…atau…

Instant instant = zdt.toInstant() ; 

Kirim ke database Anda.

myPreparedStatement.setObject(  , instant ) ;

Tentang java.time

The java.time kerangka dibangun ke Jawa 8 dan kemudian. Kelas-kelas ini menggantikan tua merepotkan warisan kelas tanggal-waktu seperti java.util.Date, Calendar, &SimpleDateFormat .

Proyek Joda-Time , sekarang dalam mode pemeliharaan , menyarankan migrasi ke java.time kelas .

Untuk mempelajari lebih lanjut, lihat Tutorial Oracle . Dan cari Stack Overflow untuk banyak contoh dan penjelasan. Spesifikasinya adalah JSR 310 .

Dimana mendapatkan kelas java.time?

Proyek ThreeTen-Extra memperluas java.time dengan kelas tambahan. Proyek ini adalah tempat pembuktian untuk kemungkinan penambahan java.time di masa mendatang. Anda mungkin menemukan beberapa kelas berguna di sini seperti Interval, YearWeek, YearQuarter, dan lebih .

Basil Bourque
sumber