Mengapa mengurangi dua kali ini (tahun 1927) memberikan hasil yang aneh?


6827

Jika saya menjalankan program berikut, yang mem-parsing dua string tanggal referensi kali 1 detik dan membandingkannya:

public static void main(String[] args) throws ParseException {
    SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    String str3 = "1927-12-31 23:54:07";  
    String str4 = "1927-12-31 23:54:08";  
    Date sDt3 = sf.parse(str3);  
    Date sDt4 = sf.parse(str4);  
    long ld3 = sDt3.getTime() /1000;  
    long ld4 = sDt4.getTime() /1000;
    System.out.println(ld4-ld3);
}

Outputnya adalah:

353

Mengapa ld4-ld3tidak 1(seperti yang saya harapkan dari perbedaan satu detik dalam waktu), tetapi 353?

Jika saya mengubah tanggal ke waktu 1 detik kemudian:

String str3 = "1927-12-31 23:54:08";  
String str4 = "1927-12-31 23:54:09";  

Maka ld4-ld3akan menjadi 1.


Versi Java:

java version "1.6.0_22"
Java(TM) SE Runtime Environment (build 1.6.0_22-b04)
Dynamic Code Evolution Client VM (build 0.2-b02-internal, 19.0-b04-internal, mixed mode)

Timezone(`TimeZone.getDefault()`):

sun.util.calendar.ZoneInfo[id="Asia/Shanghai",
offset=28800000,dstSavings=0,
useDaylight=false,
transitions=19,
lastRule=null]

Locale(Locale.getDefault()): zh_CN

23
Ini mungkin masalah lokal.
Thorbjørn Ravn Andersen

72
Jawaban sebenarnya adalah untuk selalu, selalu menggunakan detik sejak zaman untuk logging, seperti zaman Unix, dengan representasi integer 64 bit (ditandatangani, jika Anda ingin membolehkan prangko sebelum zaman). Setiap sistem waktu dunia nyata memiliki beberapa perilaku non-linear, non-monotonik seperti jam kabisat atau penghematan siang hari.
Phil H

22
Lol. Mereka memperbaikinya untuk jdk6 pada tahun 2011. Kemudian, dua tahun setelah itu, mereka menemukan bahwa itu harus diperbaiki di jdk7, juga .... diperbaiki pada 7u25, tentu saja, saya tidak menemukan petunjuk dalam catatan rilis. Terkadang saya bertanya-tanya berapa banyak bug yang diperbaiki Oracle dan tidak memberitahu siapa pun tentang hal itu karena alasan PR.
user1050755

8
Video yang bagus tentang hal-hal semacam ini: youtube.com/watch?v=-5wpm-gesOY
Thorbjørn Ravn Andersen

4
@PhilH Yang Menyenangkan adalah, masih akan ada lompatan detik. Jadi itu pun tidak berhasil.
12431234123412341234123

Jawaban:


10874

Ini adalah perubahan zona waktu pada 31 Desember di Shanghai.

Lihat halaman ini untuk perincian tahun 1927 di Shanghai. Pada dasarnya di tengah malam pada akhir 1927, jam kembali 5 menit dan 52 detik. Jadi "1927-12-31 23:54:08" sebenarnya terjadi dua kali, dan sepertinya Java menguraikannya sebagai yang mungkin nanti untuk tanggal / waktu setempat tersebut - karena itulah bedanya.

Hanya episode lain dalam dunia zona waktu yang sering aneh dan indah.

Sunting: Berhenti tekan! Perubahan sejarah ...

Pertanyaan aslinya tidak lagi menunjukkan perilaku yang sama, jika dibangun kembali dengan versi 2013a dari TZDB . Pada 2013a, hasilnya akan menjadi 358 detik, dengan waktu transisi 23:54:03 bukan 23:54:08.

Saya hanya memperhatikan ini karena saya mengumpulkan pertanyaan-pertanyaan seperti ini di Noda Time, dalam bentuk unit test ... Tes ini sekarang telah diubah, tetapi hanya untuk menunjukkan - bahkan data historis tidak aman.

EDIT: Sejarah telah berubah lagi ...

Di TZDB 2014f, waktu perubahan telah pindah ke 1900-12-31, dan sekarang hanya 343 detik perubahan (jadi waktu antara tdan t+1344 detik, jika Anda melihat apa yang saya maksud).

EDIT: Untuk menjawab pertanyaan seputar transisi pada tahun 1900 ... sepertinya implementasi zona waktu Java memperlakukan semua zona waktu hanya dalam waktu standar mereka untuk sesaat sebelum dimulainya 1900 UTC:

import java.util.TimeZone;

public class Test {
    public static void main(String[] args) throws Exception {
        long startOf1900Utc = -2208988800000L;
        for (String id : TimeZone.getAvailableIDs()) {
            TimeZone zone = TimeZone.getTimeZone(id);
            if (zone.getRawOffset() != zone.getOffset(startOf1900Utc - 1)) {
                System.out.println(id);
            }
        }
    }
}

Kode di atas tidak menghasilkan output pada mesin Windows saya. Jadi setiap zona waktu yang memiliki offset selain yang standar pada awal 1900 akan dihitung sebagai transisi. TZDB sendiri memiliki beberapa data yang kembali lebih awal dari itu, dan tidak bergantung pada gagasan waktu standar "tetap" (yang getRawOffsetdianggap sebagai konsep yang valid) sehingga perpustakaan lain tidak perlu memperkenalkan transisi buatan ini.


25
@ Jon: Karena penasaran, mengapa mereka mengatur jam mereka kembali dengan interval "aneh"? Sesuatu seperti satu jam akan tampak masuk akal, tapi kenapa jam 5: 52 menit?
Johannes Rudolph

63
@Johannes: Untuk menjadikannya zona waktu yang lebih normal secara global, saya percaya - offset yang dihasilkan adalah UTC + 8. Paris melakukan hal yang sama pada tahun 1911 misalnya: timeanddate.com/worldclock/clockchange.html?n=195&year=1911
Jon Skeet

34
@ Jon Apakah Anda tahu kalau Java / .NET berupaya dengan September pada 1752? Saya selalu suka menunjukkan orang-orang kal 9 1752 pada sistem unix
Mr Moose

30
Jadi kenapa sih Shanghai 5 menit kehabisan di tempat pertama?
Igby Largeman

25
@ Charles: Banyak tempat memiliki offset yang kurang konvensional saat itu. Di beberapa negara, masing-masing kota memiliki offset masing-masing sedekat mungkin dengan kondisi geografis.
Jon Skeet

1602

Anda telah menemukan diskontinuitas waktu setempat :

Ketika waktu standar lokal akan mencapai hari Minggu, 1. Januari 1928, jam 00:00:00 jam mundur 0:05:52 jam hingga Sabtu, 31 Desember 1927, 23:54:08 waktu standar lokal sebagai gantinya

Ini tidak terlalu aneh dan telah terjadi cukup banyak di mana-mana pada satu waktu atau yang lain ketika zona waktu diubah atau diubah karena tindakan politik atau administratif.


661

Moral dari keanehan ini adalah:

  • Gunakan tanggal dan waktu dalam UTC sedapat mungkin.
  • Jika Anda tidak dapat menampilkan tanggal atau waktu dalam UTC, selalu tunjukkan zona waktu.
  • Jika Anda tidak dapat meminta tanggal / waktu input dalam UTC, memerlukan zona waktu yang ditunjukkan secara eksplisit.

75
Konversi / penyimpanan ke UTC benar-benar tidak akan membantu untuk masalah yang dijelaskan karena Anda akan menemukan diskontinuitas dalam konversi ke UTC.
unpythonic

23
@ Markus Mann: jika program Anda menggunakan UTC secara internal di mana saja, mengonversikan ke / dari zona waktu lokal hanya di UI, Anda tidak akan peduli dengan diskontinuitas seperti itu.
Raedwald

66
@Raedwald: Tentu Anda mau - Apa waktu UTC untuk 1927-12-31 23:54:08? (Mengabaikan, untuk saat ini, UTC itu bahkan tidak ada pada tahun 1927). Pada beberapa titik waktu ini dan tanggal yang datang ke dalam sistem Anda, dan Anda harus memutuskan apa yang harus dilakukan dengan itu. Memberitahu pengguna bahwa mereka harus memasukkan waktu dalam UTC hanya memindahkan masalah kepada pengguna, itu tidak menghilangkannya.
Nick Bastin

72
Saya merasa dibenarkan atas jumlah aktivitas di utas ini, telah bekerja pada tanggal / waktu refactoring aplikasi besar selama hampir setahun sekarang. Jika Anda melakukan sesuatu seperti kalender, Anda tidak dapat "hanya" menyimpan UTC, karena definisi zona waktu tempat pembuatannya akan berubah seiring waktu. Kami menyimpan "waktu maksud pengguna" - waktu lokal pengguna dan zona waktu mereka - dan UTC untuk mencari dan menyortir, dan kapan pun basis data IANA diperbarui, kami menghitung ulang semua waktu UTC.
taiganaut

366

Ketika menambah waktu, Anda harus mengonversi kembali ke UTC dan kemudian menambah atau mengurangi. Gunakan waktu lokal hanya untuk tampilan.

Dengan cara ini Anda akan dapat berjalan melalui periode di mana jam atau menit terjadi dua kali.

Jika Anda mengonversi ke UTC, tambahkan setiap detik, dan konversi ke waktu lokal untuk tampilan. Anda akan melewati 11:54:08 pm LMT - 11:59:59 pm LMT dan kemudian 11:54:08 pm CST - 11:59:59 pm CST.


309

Alih-alih mengonversi setiap tanggal, Anda dapat menggunakan kode berikut:

long difference = (sDt4.getTime() - sDt3.getTime()) / 1000;
System.out.println(difference);

Dan kemudian lihat hasilnya:

1

72
Saya khawatir bukan itu masalahnya. Anda dapat mencoba kode saya di sistem Anda, itu akan menampilkan 1, karena kami memiliki lokal yang berbeda.
Freewind

14
Itu hanya benar karena Anda belum menentukan lokal di input parser. Itu gaya pengkodean yang buruk dan cacat desain yang besar di Jawa - lokalisasi yang melekat. Secara pribadi, saya meletakkan "TZ = UTC LC_ALL = C" di mana-mana saya menggunakan Java untuk menghindarinya. Selain itu, Anda harus menghindari setiap versi implementasi yang dilokalkan kecuali Anda berinteraksi secara langsung dengan pengguna dan secara eksplisit menginginkannya. Jangan melakukan penghitungan APAPUN termasuk pelokalan, selalu gunakan Locale.ROOT dan zona waktu UTC kecuali benar-benar diperlukan.
user1050755

226

Saya minta maaf untuk mengatakan, tetapi waktu diskontinuitas telah bergerak sedikit

JDK 6 dua tahun lalu, dan di JDK 7 baru-baru ini di pembaruan 25 .

Pelajaran untuk dipelajari: hindari waktu non-UTC di semua biaya, kecuali mungkin untuk tampilan.


27
Ini salah. Diskontinuitas bukanlah bug - hanya saja versi TZDB yang lebih baru memiliki data yang sedikit berbeda. Misalnya, pada komputer saya dengan Java 8, jika Anda mengubah kode dengan sangat sedikit menggunakan "1927-12-31 23:54:02" dan "1927-12-31 23:54:03" Anda masih akan melihat diskontinuitas - tetapi 358 detik sekarang, bukan 353. Versi TZDB yang lebih baru bahkan memiliki perbedaan lain - lihat jawaban saya untuk detailnya. Tidak ada bug nyata di sini, hanya keputusan desain tentang bagaimana nilai teks tanggal / waktu yang ambigu diuraikan.
Jon Skeet

6
Masalah sebenarnya adalah bahwa programmer tidak mengerti bahwa konversi antara waktu lokal dan universal (dalam arah mana pun) tidak dan tidak dapat diandalkan 100%. Untuk stempel waktu yang lama, data yang kami miliki tentang waktu setempat adalah yang paling tidak stabil. Untuk cap waktu di masa depan, tindakan politik dapat mengubah waktu universal mana yang diberikan peta waktu setempat. Untuk cap waktu sebelumnya dan saat ini, Anda dapat memiliki masalah bahwa proses memperbarui basis data dan meluncurkan perubahan bisa lebih lambat daripada jadwal implementasi undang-undang.
plugwash

200

Seperti yang dijelaskan oleh orang lain, ada diskontinuitas waktu di sana. Ada dua kemungkinan zona waktu untuk 1927-12-31 23:54:08at Asia/Shanghai, tetapi hanya satu offset untuk 1927-12-31 23:54:07. Jadi, tergantung pada offset mana yang digunakan, ada perbedaan satu detik atau 5 menit dan 53 detik.

Pergeseran sedikit offset ini, alih-alih penghematan siang hari satu jam (waktu musim panas) yang biasa kita gunakan, mengaburkan masalah sedikit.

Perhatikan bahwa pembaruan database zona waktu 2013a memindahkan diskontinuitas ini beberapa detik sebelumnya, tetapi efeknya masih dapat diamati.

java.timePaket baru pada Java 8 memungkinkan penggunaan melihat ini lebih jelas, dan menyediakan alat untuk menanganinya. Diberikan:

DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
dtfb.append(DateTimeFormatter.ISO_LOCAL_DATE);
dtfb.appendLiteral(' ');
dtfb.append(DateTimeFormatter.ISO_LOCAL_TIME);
DateTimeFormatter dtf = dtfb.toFormatter();
ZoneId shanghai = ZoneId.of("Asia/Shanghai");

String str3 = "1927-12-31 23:54:07";  
String str4 = "1927-12-31 23:54:08";  

ZonedDateTime zdt3 = LocalDateTime.parse(str3, dtf).atZone(shanghai);
ZonedDateTime zdt4 = LocalDateTime.parse(str4, dtf).atZone(shanghai);

Duration durationAtEarlierOffset = Duration.between(zdt3.withEarlierOffsetAtOverlap(), zdt4.withEarlierOffsetAtOverlap());

Duration durationAtLaterOffset = Duration.between(zdt3.withLaterOffsetAtOverlap(), zdt4.withLaterOffsetAtOverlap());

Maka durationAtEarlierOffsetakan menjadi satu detik, sedangkan durationAtLaterOffsetakan menjadi lima menit dan 53 detik.

Juga, kedua offset ini adalah sama:

// Both have offsets +08:05:52
ZoneOffset zo3Earlier = zdt3.withEarlierOffsetAtOverlap().getOffset();
ZoneOffset zo3Later = zdt3.withLaterOffsetAtOverlap().getOffset();

Tetapi keduanya berbeda:

// +08:05:52
ZoneOffset zo4Earlier = zdt4.withEarlierOffsetAtOverlap().getOffset();

// +08:00
ZoneOffset zo4Later = zdt4.withLaterOffsetAtOverlap().getOffset();

Anda dapat melihat masalah yang sama 1927-12-31 23:59:59dengan membandingkan 1928-01-01 00:00:00, meskipun, dalam kasus ini, offset sebelumnya yang menghasilkan divergensi yang lebih panjang, dan tanggal sebelumnya yang memiliki dua kemungkinan offset.

Cara lain untuk mendekati ini adalah untuk memeriksa apakah ada transisi yang sedang berlangsung. Kita bisa melakukan ini seperti ini:

// Null
ZoneOffsetTransition zot3 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

// An overlap transition
ZoneOffsetTransition zot4 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

Anda dapat memeriksa apakah transisi merupakan tumpang tindih di mana ada lebih dari satu offset yang valid untuk tanggal / waktu itu atau celah di mana tanggal / waktu itu tidak valid untuk id zona itu - dengan menggunakan isOverlap()dan isGap()metode pada zot4.

Saya harap ini membantu orang menangani masalah ini begitu Java 8 tersedia secara luas, atau bagi mereka yang menggunakan Java 7 yang mengadopsi JSR 310 backport.


1
Hai Daniel, saya sudah menjalankan kode Anda tetapi tidak memberikan hasil seperti yang diharapkan. seperti durationAtEarlierOffset dan durationAtLaterOffset keduanya hanya 1 detik dan juga zot3 dan zot4 keduanya nol. Saya telah menetapkan hanya menyalin dan menjalankan kode ini di mesin saya. adakah yang perlu dilakukan di sini. Beri tahu saya jika Anda ingin melihat sepotong kode. Berikut ini adalah kode tutorialspoint.com/… Anda dapat memberi tahu saya apa yang terjadi di sini.
vineeshchauhan

2
@vineeshchauhan Tergantung pada versi Java, karena ini telah berubah di tzdata, dan versi berbeda dari JDK menggabungkan berbagai versi tzdata. Pada Java yang saya instal sendiri, waktunya sedang 1900-12-31 23:54:16dan 1900-12-31 23:54:17, tetapi itu tidak berfungsi pada situs yang Anda bagikan, jadi mereka menggunakan versi Java yang berbeda dari saya.
Daniel C. Sobral

167

IMHO, lokalisasi implisit yang menyebar di Jawa adalah satu-satunya kekurangan desain terbesarnya. Ini mungkin ditujukan untuk antarmuka pengguna, tetapi terus terang, yang benar-benar menggunakan Java untuk antarmuka pengguna saat ini kecuali untuk beberapa IDE di mana Anda pada dasarnya dapat mengabaikan lokalisasi karena programmer tidak persis target audiens untuk itu. Anda dapat memperbaikinya (terutama di server Linux) dengan:

  • ekspor LC_ALL = C TZ = UTC
  • atur jam sistem Anda ke UTC
  • jangan pernah menggunakan implementasi yang dilokalkan kecuali benar-benar diperlukan (yaitu hanya untuk tampilan)

Untuk anggota Java Community Process, saya merekomendasikan:

  • membuat metode yang dilokalkan bukan default, tetapi mengharuskan pengguna untuk secara eksplisit meminta lokalisasi.
  • gunakan UTF-8 / UTC sebagai standar TETAP sebagai gantinya karena itu hanyalah standar hari ini. Tidak ada alasan untuk melakukan hal lain, kecuali jika Anda ingin membuat utas seperti ini.

Maksud saya, ayolah, bukankah variabel statis global merupakan pola anti-OO? Tidak ada yang lain adalah standar-standar pervasif yang diberikan oleh beberapa variabel lingkungan dasar .......


21

Seperti yang orang lain katakan, ini adalah perubahan waktu pada tahun 1927 di Shanghai.

Ketika itu 23:54:07di Shanghai, waktu standar lokal, tetapi setelah 5 menit dan 52 detik, itu berubah ke hari berikutnya 00:00:00, dan kemudian waktu standar lokal berubah kembali ke 23:54:08. Jadi, itulah sebabnya perbedaan antara dua kali adalah 343 detik bukan 1 detik, seperti yang Anda harapkan.

Waktu juga bisa kacau di tempat lain seperti AS. AS memiliki Daylight Saving Time. Ketika Daylight Saving Time dimulai, waktu berjalan 1 jam. Tetapi setelah beberapa saat Waktu Hemat Siang Hari berakhir, dan itu mundur 1 jam kembali ke zona waktu standar. Jadi terkadang ketika membandingkan waktu di AS perbedaannya sekitar 3600detik, bukan 1 detik.

Tetapi ada sesuatu yang berbeda tentang dua perubahan waktu ini. Yang terakhir berubah terus menerus dan yang pertama hanya perubahan. Itu tidak berubah kembali atau berubah lagi dengan jumlah yang sama.

Lebih baik menggunakan UTC di mana waktu tidak berubah kecuali jika diperlukan untuk menggunakan waktu non-UTC seperti pada layar.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.