Tại sao trừ hai lần này (năm 1927) lại cho một kết quả kỳ lạ?


6827

Nếu tôi chạy chương trình sau, phân tích hai chuỗi ngày tham chiếu cách nhau 1 giây và so sánh chúng:

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

Đầu ra là:

353

Tại sao ld4-ld3không 1(như tôi mong đợi từ sự khác biệt một giây trong thời đại), nhưng 353?

Nếu tôi thay đổi ngày thành lần 1 giây sau:

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

Rồi ld4-ld3sẽ có 1.


Phiên bản 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
Đây có thể là một vấn đề địa phương.
Thorbjørn Ravn Andersen

72
Câu trả lời thực sự là luôn luôn, luôn luôn sử dụng giây kể từ một epoch để ghi nhật ký, như epoch Unix, với biểu diễn số nguyên 64 bit (đã ký, nếu bạn muốn cho phép tem trước epoch). Bất kỳ hệ thống thời gian trong thế giới thực nào cũng có một số hành vi phi tuyến tính, không đơn điệu như giờ nhuận hoặc tiết kiệm ánh sáng ban ngày.
Phil H

22
Cười lớn. Họ đã sửa nó cho jdk6 vào năm 2011. Sau đó, hai năm sau đó, họ phát hiện ra rằng nó sẽ được sửa trong jdk7, .... cũng được sửa vào 7u25, tất nhiên, tôi không tìm thấy bất kỳ gợi ý nào trong ghi chú phát hành. Đôi khi tôi tự hỏi có bao nhiêu lỗi mà Oracle sửa và không nói cho ai biết về nó vì lý do PR.
dùng1050755

8
Một video tuyệt vời về những điều này: youtube.com/watch?v=-5wpm-gesOY
Thorbjørn Ravn Andersen

4
@PhilH Điều tuyệt vời là, vẫn sẽ có những giây nhuận. Vì vậy, ngay cả điều đó không làm việc.
12431234123412341234123

Câu trả lời:


10873

Đó là thay đổi múi giờ vào ngày 31 tháng 12 tại Thượng Hải.

Xem trang này để biết chi tiết về năm 1927 tại Thượng Hải. Về cơ bản vào nửa đêm cuối năm 1927, đồng hồ quay lại 5 phút 52 giây. Vì vậy, "1927-12-31 23:54:08" thực sự đã xảy ra hai lần và có vẻ như Java đang phân tích cú pháp như là tức thời có thể sau này cho ngày / giờ địa phương đó - do đó có sự khác biệt.

Chỉ là một tập phim khác trong thế giới múi giờ thường kỳ lạ và tuyệt vời.

EDIT: Dừng bấm! Lịch sử thay đổi ...

Câu hỏi ban đầu sẽ không còn thể hiện hành vi tương tự, nếu được xây dựng lại với phiên bản 2013a của TZDB . Trong năm 2013a, kết quả sẽ là 358 giây, với thời gian chuyển tiếp là 23:54:03 thay vì 23:54:08.

Tôi chỉ nhận thấy điều này bởi vì tôi đang thu thập các câu hỏi như thế này trong Noda Time, dưới dạng bài kiểm tra đơn vị ... Bài kiểm tra hiện đã được thay đổi, nhưng nó chỉ hiển thị - thậm chí dữ liệu lịch sử không an toàn.

EDIT: Lịch sử đã thay đổi một lần nữa ...

Trong TZDB 2014f, thời gian thay đổi đã chuyển sang 1900-12-31 và giờ chỉ là 343 giây thay đổi (vì vậy thời gian giữa tt+1là 344 giây, nếu bạn hiểu ý tôi là gì).

EDIT: Để trả lời một câu hỏi xung quanh quá trình chuyển đổi vào năm 1900 ... có vẻ như việc triển khai múi giờ Java xử lý tất cả các múi giờ như đơn giản là trong thời gian tiêu chuẩn của chúng cho bất kỳ thời điểm nào trước khi bắt đầu 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);
            }
        }
    }
}

Đoạn mã trên không tạo ra đầu ra trên máy Windows của tôi. Vì vậy, bất kỳ múi giờ nào có bất kỳ độ lệch nào ngoài thời gian chuẩn của nó vào đầu năm 1900 sẽ được coi là một chuyển đổi. Bản thân TZDB có một số dữ liệu quay lại sớm hơn thế và không dựa vào bất kỳ ý tưởng nào về thời gian tiêu chuẩn "cố định" (đó là getRawOffsetgiả định là một khái niệm hợp lệ) nên các thư viện khác không cần phải giới thiệu quá trình chuyển đổi nhân tạo này.


25
@Jon: Vì tò mò, tại sao họ lại đặt đồng hồ trở lại bởi một khoảng thời gian "kỳ lạ" như vậy? Bất cứ điều gì như một giờ sẽ có vẻ hợp lý, nhưng tại sao nó lại là 5: 52 phút?
Julian Rudolph

63
@Johannes: Để biến nó thành múi giờ bình thường hơn trên toàn cầu, tôi tin rằng - phần bù kết quả là UTC + 8. Ví dụ, Paris đã làm điều tương tự vào năm 1911: timeanddate.com/worldclock/clockchange.html?n=195&year=1911
Jon Skeet

34
@Jon Bạn có biết nếu Java / .NET đối phó với tháng 9 năm 1752 không? Tôi luôn thích cho mọi người xem cal 9 1752 trên các hệ thống unix
Mr Moose

30
Vì vậy, tại sao đầu tiên là Thượng Hải 5 phút ra khỏi wack?
Igby Largeeman

25
@Charles: Rất nhiều nơi đã có ít sự bù đắp thông thường trước đó. Ở một số quốc gia, mỗi thị trấn khác nhau đều có phần bù riêng để càng gần chính xác về mặt địa lý càng tốt.
Jon Skeet

1602

Bạn đã gặp phải gián đoạn thời gian địa phương :

Khi thời gian tiêu chuẩn địa phương chuẩn bị đến Chủ nhật, 1. Tháng 1 năm 1928, 00:00:00 đồng hồ đã bị quay ngược 0:05:52 giờ sang Thứ Bảy, 31. Tháng 12 năm 1927, 23:54:08 thay vào đó là giờ tiêu chuẩn địa phương

Điều này không phải là đặc biệt lạ và đã xảy ra khá nhiều ở mọi lúc mọi nơi khi các múi giờ bị chuyển đổi hoặc thay đổi do các hành động chính trị hoặc hành chính.


661

Đạo đức của sự kỳ lạ này là:

  • Sử dụng ngày và giờ trong UTC bất cứ nơi nào có thể.
  • Nếu bạn không thể hiển thị ngày hoặc giờ trong UTC, luôn chỉ ra múi giờ.
  • Nếu bạn không thể yêu cầu ngày / giờ đầu vào trong UTC, hãy yêu cầu múi giờ được chỉ định rõ ràng.

75
Chuyển đổi / lưu trữ thành UTC thực sự sẽ không giúp ích cho vấn đề được mô tả vì bạn sẽ gặp phải sự gián đoạn trong quá trình chuyển đổi sang UTC.
unpythonic

23
@Mark Mann: nếu chương trình của bạn sử dụng UTC nội bộ ở mọi nơi, chuyển đổi sang / từ múi giờ địa phương chỉ trong UI, bạn sẽ không quan tâm đến những điểm không liên tục như vậy.
Raedwald

66
@Raedwald: Chắc chắn bạn sẽ - Thời gian UTC cho 1927-12-31 23:54:08 là gì? (Bỏ qua, hiện tại, UTC thậm chí không tồn tại vào năm 1927). Tại một số thời điểm và ngày này đang đến trong hệ thống của bạn và bạn phải quyết định phải làm gì với nó. Nói với người dùng họ phải nhập thời gian vào UTC chỉ cần chuyển vấn đề sang người dùng, nó không loại bỏ nó.
Nick Bastin

72
Tôi cảm thấy được chứng thực ở mức độ hoạt động của chủ đề này, đã làm việc về tái cấu trúc ngày / giờ của một ứng dụng lớn trong gần một năm nay. Nếu bạn đang làm một cái gì đó như lên lịch, bạn không thể "đơn giản" lưu trữ UTC, vì các định nghĩa về múi giờ mà nó có thể được hiển thị sẽ thay đổi theo thời gian. Chúng tôi lưu trữ "thời gian mục đích của người dùng" - giờ địa phương và múi giờ của người dùng - và UTC để tìm kiếm và sắp xếp, và bất cứ khi nào cơ sở dữ liệu IANA được cập nhật, chúng tôi sẽ tính toán lại tất cả các lần UTC.
taiganaut

366

Khi tăng thời gian, bạn nên chuyển đổi trở lại UTC và sau đó cộng hoặc trừ. Chỉ sử dụng giờ địa phương để hiển thị.

Bằng cách này, bạn sẽ có thể đi bộ qua bất kỳ khoảng thời gian nào mà giờ hoặc phút xảy ra hai lần.

Nếu bạn đã chuyển đổi sang UTC, hãy thêm từng giây và chuyển đổi thành giờ địa phương để hiển thị. Bạn sẽ trải qua 11:54:08 chiều LMT - 11:59:59 tối LMT và sau đó 11:54:08 chiều CST - 11:59:59 tối CST.


309

Thay vì chuyển đổi mỗi ngày, bạn có thể sử dụng mã sau:

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

Và sau đó thấy rằng kết quả là:

1

72
Tôi sợ đó không phải là trường hợp. Bạn có thể thử mã của tôi trong hệ thống của bạn, nó sẽ xuất ra 1, bởi vì chúng tôi có các địa phương khác nhau.
Freewind

14
Điều đó chỉ đúng vì bạn chưa chỉ định miền địa phương trong đầu vào trình phân tích cú pháp. Đó là phong cách mã hóa tồi và một lỗ hổng thiết kế khổng lồ trong Java - bản địa hóa vốn có của nó. Cá nhân, tôi đặt "TZ = UTC LC_ALL = C" ở mọi nơi tôi sử dụng Java để tránh điều đó. Ngoài ra, bạn nên tránh mọi phiên bản được bản địa hóa của một triển khai trừ khi bạn đang tương tác trực tiếp với người dùng và rõ ràng muốn nó. Đừng để BẤT K to tính toán nào bao gồm cả nội địa hóa, luôn luôn sử dụng múi giờ Locale.ROOT và UTC trừ khi thực sự cần thiết.
dùng1050755

226

Tôi rất tiếc phải nói, nhưng sự gián đoạn thời gian đã chuyển một chút trong

JDK 6 hai năm trước và trong JDK 7 chỉ mới cập nhật 25 .

Bài học để tìm hiểu: tránh những lần không phải UTC bằng mọi giá, ngoại trừ có thể để hiển thị.


27
Điều này là không chính xác. Sự gián đoạn không phải là một lỗi - chỉ là phiên bản gần đây hơn của TZDB có dữ liệu hơi khác nhau. Ví dụ: trên máy của tôi có Java 8, nếu bạn thay đổi mã rất ít để sử dụng "1927-12-31 23:54:02" và "1927-12-31 23:54:03" bạn sẽ vẫn thấy gián đoạn - nhưng bây giờ là 358 giây, thay vì 353. Ngay cả các phiên bản gần đây hơn của TZDB cũng có một sự khác biệt khác - hãy xem câu trả lời của tôi để biết chi tiết. Không có lỗi thực sự ở đây, chỉ là một quyết định thiết kế xung quanh việc các giá trị văn bản ngày / thời gian mơ hồ được phân tích cú pháp như thế nào.
Jon Skeet

6
Vấn đề thực sự là các lập trình viên không hiểu rằng chuyển đổi giữa thời gian địa phương và phổ quát (theo một trong hai hướng) là không thể và không thể tin cậy 100%. Đối với dấu thời gian cũ, dữ liệu chúng tôi có về thời gian địa phương bị rung lắc nhiều nhất. Đối với dấu thời gian trong tương lai, các hành động chính trị có thể thay đổi thời gian phổ biến theo thời gian địa phương nhất định. Đối với các dấu thời gian hiện tại và gần đây, bạn có thể gặp vấn đề là quá trình cập nhật cơ sở dữ liệu tz và đưa ra các thay đổi có thể chậm hơn so với lịch trình thực thi của luật.
cắm vào

200

Theo giải thích của những người khác, có một sự gián đoạn thời gian ở đó. Có hai độ lệch múi giờ có thể có 1927-12-31 23:54:08tại Asia/Shanghai, nhưng chỉ có một độ lệch cho 1927-12-31 23:54:07. Vì vậy, tùy thuộc vào mức bù nào được sử dụng, sẽ có chênh lệch một giây hoặc chênh lệch 5 phút và 53 giây.

Sự thay đổi nhỏ này của sự bù đắp, thay vì tiết kiệm ánh sáng ban ngày một giờ (thời gian mùa hè) thông thường mà chúng ta đã quen, che khuất vấn đề một chút.

Lưu ý rằng bản cập nhật 2013a của cơ sở dữ liệu múi giờ đã di chuyển sự gián đoạn này vài giây trước đó, nhưng hiệu quả vẫn có thể quan sát được.

java.timeGói mới trên Java 8 cho phép sử dụng thấy rõ hơn điều này và cung cấp các công cụ để xử lý nó. Được:

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

Sau đó durationAtEarlierOffsetsẽ là một giây, trong khi durationAtLaterOffsetsẽ là năm phút và 53 giây.

Ngoài ra, hai offset này giống nhau:

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

Nhưng hai cái này thì khác:

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

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

Tuy nhiên, trong trường hợp này, bạn có thể thấy cùng một vấn đề so 1927-12-31 23:59:59với 1928-01-01 00:00:00, trong trường hợp này, chính phần bù trước đó tạo ra sự phân kỳ dài hơn và đó là ngày sớm hơn có hai lần bù trừ có thể xảy ra.

Một cách khác để tiếp cận điều này là kiểm tra xem liệu có sự chuyển đổi đang diễn ra hay không. Chúng ta có thể làm điều này như thế này:

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

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

Bạn có thể kiểm tra xem quá trình chuyển đổi có trùng lặp hay không khi có nhiều hơn một khoảng bù hợp lệ cho ngày / giờ đó hoặc khoảng cách trong đó ngày / giờ đó không hợp lệ cho id vùng đó - bằng cách sử dụng isOverlap()isGap()phương thức trên zot4.

Tôi hy vọng điều này sẽ giúp mọi người xử lý loại vấn đề này một khi Java 8 trở nên phổ biến rộng rãi hoặc cho những người sử dụng Java 7 sử dụng backport JSR 310.


1
Xin chào Daniel, tôi đã chạy đoạn mã của bạn nhưng nó không cho đầu ra như mong đợi. như thời gianAtEarlier Offerset và thời gianAtLater Offerset cả hai chỉ 1 giây và cả zot3 và zot4 đều là null. Tôi vừa thiết lập vừa sao chép và chạy mã này trên máy của mình. Có bất cứ điều gì cần phải được thực hiện ở đây. Hãy cho tôi biết nếu bạn muốn xem một đoạn mã. Đây là mã hướng dẫnspoint.com / Bạn có thể cho tôi biết những gì đang xảy ra ở đây.
Vineeshchauhan

2
@vineeshchauhan Nó phụ thuộc vào phiên bản của Java, vì điều này đã thay đổi trong tzdata và các phiên bản khác nhau của JDK gói các phiên bản khác nhau của tzdata. Trên Java đã cài đặt của riêng tôi, thời gian là 1900-12-31 23:54:161900-12-31 23:54:17, nhưng nó không hoạt động trên trang web bạn đã chia sẻ, vì vậy họ đang sử dụng một phiên bản Java khác với I.
Daniel C. Sobral

167

IMHO sự phổ biến, nội địa hóa ngầm trong Java là lỗ hổng thiết kế lớn nhất của nó. Nó có thể dành cho giao diện người dùng, nhưng thật lòng mà nói, người thực sự sử dụng Java cho giao diện người dùng ngày nay ngoại trừ một số IDE nơi bạn có thể bỏ qua việc bản địa hóa vì các lập trình viên không chính xác là đối tượng mục tiêu cho nó. Bạn có thể sửa nó (đặc biệt là trên các máy chủ Linux) bằng cách:

  • xuất LC_ALL = C TZ = UTC
  • đặt đồng hồ hệ thống của bạn thành UTC
  • không bao giờ sử dụng triển khai cục bộ trừ khi thực sự cần thiết (nghĩa là chỉ hiển thị)

Đối với các thành viên Quy trình cộng đồng Java, tôi khuyên bạn nên:

  • làm cho các phương thức được bản địa hóa không phải là mặc định, nhưng yêu cầu người dùng yêu cầu rõ ràng bản địa hóa.
  • thay vào đó, hãy sử dụng UTF-8 / UTC làm mặc định CỐ ĐỊNH vì đó đơn giản là mặc định ngày nay. Không có lý do để làm một cái gì đó khác, ngoại trừ nếu bạn muốn sản xuất các chủ đề như thế này.

Ý tôi là, thôi nào, không phải các biến tĩnh toàn cầu là một mẫu chống OO sao? Không có gì khác là những mặc định phổ biến được đưa ra bởi một số biến môi trường thô sơ .......


21

Như những người khác nói, đó là một sự thay đổi thời gian vào năm 1927 tại Thượng Hải.

Khi nó 23:54:07ở Thượng Hải, giờ tiêu chuẩn địa phương, nhưng sau 5 phút và 52 giây, nó đã chuyển sang ngày hôm sau 00:00:00, và sau đó thời gian tiêu chuẩn địa phương đổi lại 23:54:08. Vì vậy, đó là lý do tại sao sự khác biệt giữa hai lần là 343 giây chứ không phải 1 giây, như bạn mong đợi.

Thời gian cũng có thể gây rối ở những nơi khác như Mỹ. Hoa Kỳ có Giờ tiết kiệm ánh sáng ban ngày. Khi Thời gian tiết kiệm ánh sáng ban ngày bắt đầu, thời gian sẽ tiếp tục 1 giờ. Nhưng sau một thời gian, Giờ tiết kiệm ánh sáng ban ngày kết thúc và nó lùi về 1 giờ so với múi giờ tiêu chuẩn. Vì vậy, đôi khi khi so sánh thời gian ở Mỹ, sự khác biệt là khoảng 3600giây chứ không phải 1 giây.

Nhưng có một cái gì đó khác nhau về hai thay đổi thời gian này. Cái sau thay đổi liên tục và cái trước chỉ là một sự thay đổi. Nó không thay đổi trở lại hoặc thay đổi một lần nữa với cùng một số tiền.

Tốt hơn là sử dụng UTC khi thời gian không thay đổi trừ khi cần sử dụng thời gian không phải UTC như trong màn hình.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.