Làm thế nào để xử lý các múi giờ lịch bằng Java?


92

Tôi có một giá trị Dấu thời gian đến từ ứng dụng của mình. Người dùng có thể ở bất kỳ TimeZone cục bộ nào.

Vì ngày này được sử dụng cho một WebService giả định thời gian đã cho luôn là GMT, tôi cần chuyển đổi thông số của người dùng từ say (EST) sang (GMT). Đây là kicker: Người dùng không biết gì về TZ của mình. Anh ấy nhập ngày tạo mà anh ấy muốn gửi đến WS, vì vậy những gì tôi cần là:

Người dùng nhập: 5/1/2008 6:12 PM (EST)
Tham số cho WS cần phải là : 5/1/2008 6:12 PM (GMT)

Tôi biết TimeStamps luôn được cho là theo GMT theo mặc định, nhưng khi gửi tham số, mặc dù tôi đã tạo Lịch của mình từ TS (được cho là tính theo GMT), giờ luôn tắt trừ khi người dùng ở GMT. Tôi đang thiếu gì?

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

Với Mã trước, đây là kết quả tôi nhận được (Định dạng ngắn để dễ đọc):

[Ngày 1 tháng 5 năm 2008 11:12 CH]


2
Tại sao bạn chỉ thay đổi múi giờ mà không chuyển đổi ngày / giờ cùng với nó?
Spencer Kormos 23/10/08

1
Một nỗi đau thực sự của nó để có một ngày Java có nghĩa là trong một múi giờ, và nhận được rằng ngày trong múi giờ khác. IE, lấy 5PM EDT và lấy 5PM PDT.
mtyson

Câu trả lời:


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

Đây là kết quả nếu tôi vượt qua thời gian hiện tại ("12:09:05 EDT" từ Calendar.getInstance()) trong:

GỠ LỖI - lịch đầu vào có ngày [Thứ Sáu 23 tháng 10 12:09:05 EDT 2008]
GỠ LỖI - chênh lệch là -14400000 GỢI Ý
- Đã tạo theo GMT cal với ngày [Thứ Sáu 23 tháng 10 08:09:05 EDT 2008]

12:09:05 GMT là 8:09:05 EDT.

Phần khó hiểu ở đây là Calendar.getTime()trả về cho bạn Datemúi giờ hiện tại của bạn và cũng không có phương pháp nào để sửa đổi múi giờ của lịch và cũng có ngày cơ bản được đưa vào. Tùy thuộc vào loại thông số mà dịch vụ web của bạn sử dụng, bạn có thể chỉ muốn có thỏa thuận WS theo mili giây từ kỷ nguyên.


11
Bạn không nên trừ offsetFromUTCthay vì cộng nó? Sử dụng ví dụ của bạn, nếu 12:09 GMT là 8:09 EDT (đúng) và người dùng nhập "12:09 EDT", theo ý kiến ​​của tôi, thuật toán sẽ xuất ra "16:09 GMT".
DzinX

29

Cảm ơn tất cả các bạn để đáp ứng. Sau khi điều tra thêm, tôi đã có câu trả lời đúng. Như đã đề cập bởi Skip Head, TimeStamped mà tôi nhận được từ ứng dụng của mình đã được điều chỉnh thành TimeZone của người dùng. Vì vậy, nếu Người dùng nhập 6:12 PM (EST), tôi sẽ nhận được 2:12 PM (GMT). Những gì tôi cần là một cách để hoàn tác chuyển đổi sao cho thời gian mà người dùng đã nhập là thời gian tôi gửi đến yêu cầu WebServer. Đây là cách tôi thực hiện điều này:

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

Đầu ra của mã là: (Người dùng đã nhập 5/1/2008 6:12 CH (EST)


Giờ của người dùng hiện tại Múi giờ : EST Chênh lệch hiện tại từ GMT (tính theo giờ): - 4 (Thông thường -5, ngoại trừ được điều chỉnh theo DST)
TS từ ACP: 2008-05-01 14: 12: 00.0
Ngày lịch được chuyển đổi từ TS bằng cách sử dụng GMT và US_EN Locale : 5/1/08 6:12 PM (GMT)


20

Bạn nói rằng ngày được sử dụng liên quan đến các dịch vụ web, vì vậy tôi giả sử rằng ngày đó được tuần tự hóa thành một chuỗi tại một số điểm.

Nếu đúng như vậy, bạn nên xem phương thức setTimeZone của lớp DateFormat. Điều này cho biết múi giờ nào sẽ được sử dụng khi in tem thời gian.

Một ví dụ đơn giản:

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

4
Không liên quan đến SDF Đã có múi giờ riêng, tự hỏi tại sao việc thay đổi Lịch Thời gian dường như không có tác dụng!
Chris.Jenkins

TimeZone.getTimeZone ("UTC") không hợp lệ vì UTC không có trong AvailableIDs () ... chúa biết tại sao
childno͡.de 14/09/12

TimeZone.getTimeZone ("UTC") có sẵn trên máy của tôi. Nó có phụ thuộc JVM không?
CodeClimber

2
Ý tưởng tuyệt vời với phương thức setTimeZone (). Bạn đã cứu tôi rất nhiều đau đầu, cảm ơn rất nhiều!
Bogdan Zurac

1
Đúng thứ tôi cần! Bạn có thể đặt múi giờ máy chủ trong trình định dạng và sau đó khi bạn chuyển đổi nó sang định dạng lịch, bạn không có gì phải lo lắng. Phương pháp tốt nhất cho đến nay! đối với getTimeZone, bạn có thể cần sử dụng định dạng Ví dụ: "GMT-4: 00" cho ETD
Michael Kern

12

Bạn có thể giải quyết nó với 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"));

Tuy nhiên, hãy quan tâm đến điều này nếu bạn đang sử dụng hibernate 4, không tương thích trực tiếp mà không có phụ thuộc bên và cấu hình bổ sung. Tuy nhiên, đó là cách tiếp cận nhanh nhất và dễ sử dụng nhất cho phiên bản thứ 3.
Aubergine

8

Có vẻ như TimeStamp của bạn đang được đặt thành múi giờ của hệ thống gốc.

Điều này không được dùng nữa, nhưng nó sẽ hoạt động:

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

Cách không bị phản đối là sử dụng

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

nhưng điều đó sẽ cần được thực hiện ở phía máy khách, vì hệ thống đó biết múi giờ của nó.


7

Phương pháp chuyển đổi từ timeZone này sang timeZone khác (có thể nó hoạt động :)).

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

6

Các đối tượng DateTimestamp không rõ ràng về múi giờ: chúng đại diện cho một số giây nhất định kể từ kỷ nguyên, mà không cam kết diễn giải cụ thể thời điểm đó là giờ và ngày. Múi giờ chỉ nhập ảnh trong GregorianCalendar (không cần thiết trực tiếp cho tác vụ này) và SimpleDateFormat , cần có độ lệch múi giờ để chuyển đổi giữa các trường riêng biệt và Ngày (hoặc dài giá trị ).

Vấn đề của OP nằm ngay khi bắt đầu xử lý: người dùng nhập giờ, không rõ ràng và chúng được diễn giải theo múi giờ địa phương, không phải GMT; tại thời điểm này, giá trị là "6:12 EST" , có thể dễ dàng in thành "11,12 GMT" hoặc bất kỳ múi giờ nào khác nhưng sẽ không bao giờ thay đổi thành "6,12 GMT" .

Không có cách nào để làm cho SimpleDateFormat phân tích cú pháp "06:12""HH: MM" (mặc định là múi giờ địa phương) thay vào đó là UTC; SimpleDateFormat hơi quá thông minh so với lợi ích của nó.

Tuy nhiên, bạn có thể thuyết phục bất kỳ phiên bản SimpleDateFormat sử dụng múi giờ phù hợp nếu bạn đặt nó một cách rõ ràng trong đầu vào: chỉ cần nối một chuỗi cố định vào "06:12" đã nhận (và được xác thực đầy đủ) để phân tích cú pháp "06:12 GMT""HH: MM z" .

Không cần thiết lập GregorianCalendar rõ ràng trường hoặc truy xuất và sử dụng múi giờ và khoảng thời gian tiết kiệm ánh sáng ban ngày.

Vấn đề thực sự là tách biệt các đầu vào mặc định theo múi giờ cục bộ, đầu vào mặc định thành UTC và đầu vào thực sự yêu cầu chỉ báo múi giờ rõ ràng.


4

Điều gì đó đã làm việc với tôi trong quá khứ là xác định độ lệch (tính bằng mili giây) giữa múi giờ của người dùng và GMT. Khi bạn đã có giá trị chênh lệch, bạn có thể chỉ cần cộng / trừ (tùy thuộc vào cách chuyển đổi diễn ra) để có được thời gian thích hợp trong một trong hai múi giờ. Tôi thường thực hiện điều này bằng cách đặt trường mili giây của đối tượng Lịch, nhưng tôi chắc rằng bạn có thể dễ dàng áp dụng nó cho đối tượng dấu thời gian. Đây là mã tôi sử dụng để lấy phần bù

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

timezoneId là id của múi giờ của người dùng (chẳng hạn như EST).


9
Sử dụng phần bù thô bạn đang bỏ qua DST.
Werner Lehmann

1
Chính xác, phương pháp này sẽ cho kết quả không chính xác trong nửa năm. Dạng lỗi tồi tệ nhất.
Danubian Sailor:

1

java.time

Cách tiếp cận hiện đại sử dụng các lớp java.time thay thế các lớp ngày-giờ kế thừa rắc rối được đóng gói trong các phiên bản Java sớm nhất.

Các java.sql.Timestamplớp học là một trong những lớp học di sản. Không cần nữa. Thay vào đó, hãy sử dụng Instanthoặc các lớp java.time khác trực tiếp với cơ sở dữ liệu của bạn bằng JDBC 4.2 trở lên.

Các Instant lớp đại diện cho một thời điểm trên Timeline trong UTC với độ phân giải nano giây (lên đến chín (9) chữ số của một phân số thập phân).

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

Nếu bạn phải tương tác với một lớp hiện có Timestamp, hãy chuyển đổi ngay lập tức thành java.time thông qua các phương thức chuyển đổi mới được thêm vào các lớp cũ.

Instant instant = myTimestamp.toInstant() ;

Để điều chỉnh thành múi giờ khác, hãy chỉ định múi giờ làm ZoneIdđối tượng. Chỉ định một tên múi giờ thích hợp trong các định dạng của continent/region, chẳng hạn như America/Montreal, Africa/CasablancahoặcPacific/Auckland . Không bao giờ sử dụng các múi giờ giả 3-4 chữ cái, chẳng hạn như ESThoặc ISTchúng không phải là múi giờ thực, không được tiêu chuẩn hóa và thậm chí không phải là duy nhất (!).

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

Áp dụng cho Instantđể tạo ra một ZonedDateTimeđối tượng.

ZonedDateTime zdt = instant.atZone( z ) ;

Để tạo một chuỗi để hiển thị cho người dùng, hãy tìm kiếm Stack Overflow DateTimeFormatterđể tìm nhiều cuộc thảo luận và ví dụ.

Câu hỏi của bạn thực sự là đi theo hướng khác, từ mục nhập dữ liệu của người dùng đến các đối tượng ngày-giờ. Nói chung, tốt nhất nên chia mục nhập dữ liệu của bạn thành hai phần, ngày và giờ trong ngày.

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

Câu hỏi của bạn không rõ ràng. Bạn có muốn diễn giải ngày và giờ do người dùng nhập thành UTC không? Hay ở múi giờ khác?

Nếu bạn muốn nói đến UTC, hãy tạo một OffsetDateTimevới một khoảng bù bằng cách sử dụng hằng số cho UTC ZoneOffset.UTC,.

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

Nếu bạn muốn nói đến một múi giờ khác, hãy kết hợp cùng với một đối tượng múi giờ, a ZoneId. Nhưng múi giờ nào? Bạn có thể phát hiện một múi giờ mặc định. Hoặc, nếu quan trọng, bạn phải xác nhận với người dùng để chắc chắn về ý định của họ.

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

Để có được một đối tượng đơn giản hơn luôn ở UTC theo định nghĩa, hãy trích xuất một Instant.

Instant instant = odt.toInstant() ;

…hoặc là…

Instant instant = zdt.toInstant() ; 

Gửi đến cơ sở dữ liệu của bạn.

myPreparedStatement.setObject(  , instant ) ;

Giới thiệu về java.time

Khung java.time được tích hợp sẵn trong Java 8 trở lên. Những lớp học thay thế cái cũ phiền hà di sản lớp học ngày thời gian như java.util.Date, Calendar, &SimpleDateFormat .

Các Joda thời gian dự án, bây giờ trong chế độ bảo trì , khuyên chuyển đổi sang các java.time lớp.

Để tìm hiểu thêm, hãy xem Hướng dẫn Oracle . Và tìm kiếm Stack Overflow để có nhiều ví dụ và giải thích. Đặc điểm kỹ thuật là JSR 310 .

Lấy các lớp java.time ở đâu?

  • Java SE 8 , Java SE 9 và mới hơn
    • Được xây dựng trong.
    • Một phần của API Java tiêu chuẩn với triển khai theo gói.
    • Java 9 bổ sung một số tính năng nhỏ và các bản sửa lỗi.
  • Java SE 6 Java SE 7
    • Phần lớn chức năng của java.time được chuyển ngược sang Java 6 & 7 trong ThreeTen-Backport .
  • Android
    • Các phiên bản triển khai gói Android mới hơn của các lớp java.time.
    • Đối với Android trước đó, ThreeTenABP dự án thích nghi ThreeTen-backport (nêu trên). Xem Cách sử dụng ThreeTenABP… .

Các ThreeTen-Extra dự án mở rộng java.time với các lớp bổ sung. Dự án này là cơ sở chứng minh cho những bổ sung có thể có trong tương lai cho java.time. Bạn có thể tìm thấy một số các lớp học hữu ích ở đây chẳng hạn như Interval, YearWeek, YearQuarter, và nhiều hơn nữa .

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.