วิธีจัดการปฏิทิน TimeZones โดยใช้ Java?


92

ฉันมีค่า Timestamp ที่มาจากแอปพลิเคชันของฉัน ผู้ใช้สามารถอยู่ในเขตเวลาท้องถิ่นใดก็ได้

เนื่องจากวันที่นี้ใช้สำหรับ WebService ซึ่งถือว่าเวลาที่กำหนดเป็น GMT เสมอฉันจึงจำเป็นต้องแปลงพารามิเตอร์ของผู้ใช้จาก say (EST) เป็น (GMT) นี่คือตัวเตะ: ผู้ใช้ไม่สนใจ TZ ของเขา เขาป้อนวันที่สร้างที่เขาต้องการส่งไปยัง WS ดังนั้นสิ่งที่ฉันต้องการคือ:

ผู้ใช้เข้าสู่: 5/1/2008 18:12 PM (EST)
พารามิเตอร์ของ WS ต้องเป็น : 5/1/2008 18:12 PM (GMT)

ฉันรู้ว่า TimeStamps ควรอยู่ใน GMT โดยค่าเริ่มต้นเสมอ แต่เมื่อส่งพารามิเตอร์แม้ว่าฉันจะสร้างปฏิทินจาก TS (ซึ่งควรจะเป็น GMT) ชั่วโมงจะปิดตลอดเวลาเว้นแต่ผู้ใช้จะอยู่ใน GMT ฉันขาดอะไรไป?

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

ด้วยรหัสก่อนหน้านี่คือสิ่งที่ฉันได้รับจากผลลัพธ์ (รูปแบบย่อเพื่อให้อ่านง่าย):

[1 พฤษภาคม 2551 23:12 น.]


2
เหตุใดคุณจึงเปลี่ยนเพียงเขตเวลาและไม่แปลงวันที่ / เวลาควบคู่ไปด้วย
Spencer Kormos

1
เป็นความเจ็บปวดอย่างแท้จริงที่ต้องใช้วันที่ Java ที่อยู่ในเขตเวลาเดียวและรับวันที่นั้นในเขตเวลาอื่น IE ใช้เวลา 17.00 น. EDT และรับ 17.00 น. PDT
mtyson

คำตอบ:


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

นี่คือผลลัพธ์ถ้าฉันผ่านเวลาปัจจุบัน ("12:09:05 EDT" จากCalendar.getInstance()) ใน:

DEBUG - ปฏิทินอินพุตมีวันที่ [พฤ 23 ต.ค. 12:09:05 EDT 2008]
DEBUG - offset คือ -14400000
DEBUG - สร้าง GMT cal พร้อมวันที่ [พฤหัสบดี 23 ต.ค. 08:09:05 EDT 2008]

12:09:05 GMT คือ 8:09:05 EDT

ส่วนที่ทำให้เกิดความสับสนในที่นี้คือCalendar.getTime()ส่งคืนคุณDateในเขตเวลาปัจจุบันของคุณและยังไม่มีวิธีใดในการแก้ไขเขตเวลาของปฏิทินและมีการย้อนวันที่ที่เป็นพื้นฐานด้วย ขึ้นอยู่กับประเภทของพารามิเตอร์ที่บริการเว็บของคุณใช้คุณอาจต้องการเพียงแค่มีดีล WS ในรูปของมิลลิวินาทีจากยุค


11
คุณไม่ควรลบoffsetFromUTCแทนการบวกหรือไม่? โดยใช้ตัวอย่างของคุณถ้า 12:09 GMT คือ 8:09 EDT (ซึ่งเป็นจริง) และผู้ใช้ป้อน "12:09 EDT" อัลกอริทึมควรแสดงผล "16:09 GMT" ในความคิดของฉัน
DzinX

29

ขอบคุณทุกท่านที่ตอบกลับ หลังจากการตรวจสอบเพิ่มเติมฉันได้คำตอบที่ถูกต้อง ตามที่กล่าวไว้โดย Skip Head TimeStamped ที่ฉันได้รับจากแอปพลิเคชันของฉันถูกปรับให้เป็น TimeZone ของผู้ใช้ ดังนั้นหากผู้ใช้ป้อนเวลา 18:12 น. (EST) ฉันจะได้รับ 14:12 น. (GMT) สิ่งที่ฉันต้องการคือวิธียกเลิกการแปลงเพื่อให้เวลาที่ผู้ใช้ป้อนคือเวลาที่ฉันส่งไปยังคำขอ WebServer นี่คือวิธีที่ฉันทำสิ่งนี้ให้สำเร็จ:

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

เอาต์พุตของรหัสคือ: (ผู้ใช้ป้อน 5/1/2008 18:12 น. (EST)

เขตเวลาของผู้ใช้ปัจจุบัน: EST
ออฟเซ็ตปัจจุบันจาก GMT (เป็นชม.): - 4 (ปกติ -5 ยกเว้นจะปรับ DST)
TS จาก ACP: 2008-05-01 14: 12: 00.0
วันที่ในปฏิทินที่แปลงจาก TS โดยใช้ GMT และ US_EN Locale : 5/1/08 18:12 น. (GMT)


20

คุณบอกว่าวันที่ถูกใช้ในการเชื่อมต่อกับบริการบนเว็บดังนั้นฉันจึงถือว่าเป็นอนุกรมเป็นสตริงในบางจุด

หากเป็นกรณีนี้คุณควรดูเมธอด setTimeZoneของคลาส DateFormat สิ่งนี้กำหนดเขตเวลาที่จะใช้เมื่อพิมพ์การประทับเวลา

ตัวอย่างง่ายๆ:

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
ไม่เกี่ยว SDF มีเขตเวลาของตัวเองสงสัยว่าทำไมการเปลี่ยน TimeZone ของปฏิทินดูเหมือนจะไม่มีผล!
Chris Jenkins

TimeZone.getTimeZone ("UTC") ไม่ถูกต้องเนื่องจาก UTC ไม่อยู่ใน AvailableIDs () ... พระเจ้ารู้ว่าทำไม
childno͡.de

TimeZone.getTimeZone ("UTC") อยู่ในเครื่องของฉัน JVM ขึ้นอยู่กับ JVM หรือไม่?
CodeClimber

2
ไอเดียสุดเจ๋งด้วยเมธอด setTimeZone () คุณช่วยฉันปวดหัวมากขอบคุณมาก!
Bogdan Zurac

1
สิ่งที่ฉันต้องการ! คุณสามารถตั้งค่าโซนเวลาของเซิร์ฟเวอร์ในฟอร์แมตเตอร์และเมื่อคุณแปลงเป็นรูปแบบปฏิทินคุณก็ไม่มีอะไรต้องกังวล วิธีที่ดีที่สุด! สำหรับ getTimeZone คุณอาจต้องใช้รูปแบบเช่น: "GMT-4: 00" สำหรับ ETD
Michael Kern

12

คุณสามารถแก้ปัญหาได้ด้วย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"));

อย่างไรก็ตามหากคุณใช้โหมดไฮเบอร์เนต 4 ซึ่งไม่สามารถใช้งานร่วมกันได้โดยตรงหากไม่มีการพึ่งพาด้านข้างและการกำหนดค่าเพิ่มเติม อย่างไรก็ตามมันเป็นวิธีที่เร็วและง่ายที่สุดสำหรับเวอร์ชันที่ 3
Aubergine

8

ดูเหมือนว่า TimeStamp ของคุณจะถูกตั้งค่าเป็นเขตเวลาของระบบต้นทาง

สิ่งนี้เลิกใช้แล้ว แต่ควรใช้งานได้:

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

วิธีที่ไม่เลิกใช้คือการใช้

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

แต่ต้องทำในฝั่งไคลเอ็นต์เนื่องจากระบบดังกล่าวรู้ว่าอยู่ในเขตเวลาใด


7

วิธีการแปลงจากโซนเวลาหนึ่งไปเป็นโซนอื่น (น่าจะใช้ได้ :))

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

อบเจ็กต์วันที่และเวลาเป็นเขตเวลาที่ไม่สามารถลบเลือนได้: พวกมันแสดงจำนวนวินาทีที่แน่นอนตั้งแต่ยุคสมัยโดยไม่ต้องตีความเฉพาะเจาะจงของเวลานั้นเป็นชั่วโมงและวัน เขตเวลาป้อนรูปภาพในGregorianCalendarเท่านั้น(ไม่จำเป็นสำหรับงานนี้โดยตรง) และSimpleDateFormatซึ่งต้องใช้ค่าชดเชยเขตเวลาเพื่อแปลงระหว่างเขตข้อมูลที่แยกจากกันและค่าวันที่ (หรือยาว )

ปัญหาของ OP อยู่ที่จุดเริ่มต้นของการประมวลผล: ผู้ใช้ป้อนชั่วโมงซึ่งมีความคลุมเครือและมีการตีความตามเขตเวลาท้องถิ่นที่ไม่ใช่ GMT ที่จุดนี้มีค่าเป็น"06:12 EST"ซึ่งสามารถพิมพ์ได้อย่างง่ายดายเป็น"11.12 GMT"หรือเขตอื่น ๆ แต่จะไม่ไปเปลี่ยนแปลงไป"6.12 GMT"

ไม่มีวิธีใดที่จะทำให้SimpleDateFormatที่แยกวิเคราะห์"06:12"เป็น"HH: MM" (ค่าเริ่มต้นเป็นเขตเวลาท้องถิ่น) เริ่มต้นเป็น UTC แทน SimpleDateFormatฉลาดเกินไปสำหรับความดีของตัวเอง

อย่างไรก็ตามคุณสามารถโน้มน้าวให้อินสแตนซ์SimpleDateFormatใช้เขตเวลาที่ถูกต้องได้หากคุณใส่ไว้อย่างชัดเจนในอินพุต: เพียงต่อท้ายสตริงคงที่กับที่ได้รับ (และตรวจสอบอย่างเพียงพอ) "06:12"เพื่อแยกวิเคราะห์"06:12 GMT"เป็น"HH: MM Z"

ไม่จำเป็นต้องมีการตั้งค่าฟิลด์GregorianCalendarอย่างชัดเจนหรือการดึงข้อมูลและใช้เขตเวลาและการชดเชยเวลาออมแสง

ปัญหาที่แท้จริงคือการแยกอินพุตที่เป็นค่าเริ่มต้นเป็นเขตเวลาท้องถิ่นอินพุตที่เป็นค่าเริ่มต้นเป็น UTC และอินพุตที่ต้องการการระบุเขตเวลาที่ชัดเจนจริงๆ


4

สิ่งที่ได้ผลสำหรับฉันในอดีตคือการกำหนดค่าชดเชย (ในหน่วยมิลลิวินาที) ระหว่างเขตเวลาของผู้ใช้และ GMT เมื่อคุณได้ค่าชดเชยแล้วคุณสามารถเพิ่ม / ลบ (ขึ้นอยู่กับว่า Conversion จะไปทางใด) เพื่อให้ได้เวลาที่เหมาะสมในเขตเวลาใดเขตหนึ่ง โดยปกติฉันจะทำได้โดยการตั้งค่าฟิลด์มิลลิวินาทีของวัตถุปฏิทิน แต่ฉันแน่ใจว่าคุณสามารถใช้กับวัตถุประทับเวลาได้อย่างง่ายดาย นี่คือรหัสที่ฉันใช้เพื่อรับค่าชดเชย

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

timezoneId คือรหัสของเขตเวลาของผู้ใช้ (เช่น EST)


9
การใช้ค่าชดเชยดิบคุณกำลังละเว้น DST
Werner Lehmann

1
วิธีนี้จะให้ผลลัพธ์ที่ไม่ถูกต้องเป็นเวลาครึ่งปี ข้อผิดพลาดที่เลวร้ายที่สุด
Danubian Sailor

1

java.time

วิธีการที่ทันสมัยใช้คลาสjava.timeที่แทนที่คลาสวันที่และเวลาเดิมที่มีปัญหาซึ่งมาพร้อมกับ Java เวอร์ชันแรกสุด

java.sql.Timestampชั้นเป็นหนึ่งในชั้นเรียนมรดกเหล่านั้น ไม่ต้องการอีกต่อไป. แทนที่จะใช้Instantหรือคลาส java.time อื่น ๆ โดยตรงกับฐานข้อมูลของคุณโดยใช้ JDBC 4.2 และใหม่กว่า

Instantชั้นหมายถึงช่วงเวลาบนไทม์ไลน์ในส่วนUTCมีความละเอียดของนาโนวินาที (ถึงเก้า (9) ตัวเลขของส่วนทศนิยม)

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

หากคุณต้องทำงานร่วมกับสิ่งที่มีอยู่Timestampให้แปลงเป็น java.time ทันทีโดยใช้วิธีการแปลงใหม่ที่เพิ่มลงในคลาสเก่า

Instant instant = myTimestamp.toInstant() ;

หากต้องการปรับเป็นเขตเวลาอื่นให้ระบุเขตเวลาเป็นZoneIdวัตถุ ระบุชื่อโซนเวลาที่เหมาะสมในรูปแบบของcontinent/regionเช่นAmerica/Montreal, หรือAfrica/Casablanca Pacific/Aucklandห้ามใช้โซนหลอกตัวอักษร 3-4 ตัวเช่นESTหรือISTเนื่องจากไม่ใช่โซนเวลาจริงไม่เป็นมาตรฐานและไม่ซ้ำกัน (!)

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

นำไปใช้กับการInstantสร้างZonedDateTimeวัตถุ

ZonedDateTime zdt = instant.atZone( z ) ;

หากต้องการสร้างสตริงสำหรับแสดงต่อผู้ใช้ให้ค้นหา Stack Overflow DateTimeFormatterเพื่อค้นหาการอภิปรายและตัวอย่างมากมาย

คำถามของคุณเกี่ยวกับการไปในทิศทางอื่นตั้งแต่การป้อนข้อมูลผู้ใช้ไปจนถึงวัตถุวันที่และเวลา โดยทั่วไปแล้ววิธีที่ดีที่สุดคือแบ่งการป้อนข้อมูลของคุณออกเป็นสองส่วนวันที่และเวลาของวัน

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

คำถามของคุณไม่ชัดเจน คุณต้องการตีความวันที่และเวลาที่ผู้ใช้ป้อนให้เป็น UTC หรือไม่? หรือในเขตเวลาอื่น?

ถ้าคุณหมายถึง UTC สร้างOffsetDateTimeด้วยการชดเชยการใช้อย่างต่อเนื่องสำหรับ ZoneOffset.UTCUTC,

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

หากคุณหมายถึงเขตเวลาอื่นให้รวมเข้ากับวัตถุเขตเวลา a ZoneId. แต่เขตเวลาใด คุณอาจตรวจพบเขตเวลาเริ่มต้น หรือหากสำคัญคุณต้องยืนยันกับผู้ใช้เพื่อให้แน่ใจว่าเจตนาของพวกเขา

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

เพื่อให้ได้วัตถุที่ง่ายกว่าซึ่งอยู่ใน UTC ตามคำนิยามเสมอให้แยกInstantไฟล์.

Instant instant = odt.toInstant() ;

…หรือ…

Instant instant = zdt.toInstant() ; 

ส่งไปยังฐานข้อมูลของคุณ

myPreparedStatement.setObject( … , instant ) ;

เกี่ยวกับ java.time

java.timeกรอบถูกสร้างขึ้นใน Java 8 และต่อมา ชั้นเรียนเหล่านี้แย่งลำบากเก่ามรดกเรียนวันที่เวลาเช่นjava.util.Date, และCalendarSimpleDateFormat

โครงการJoda-Timeขณะนี้อยู่ในโหมดการบำรุงรักษาแนะนำให้ย้ายไปยังคลาสjava.time

ต้องการเรียนรู้เพิ่มเติมโปรดดูที่ออราเคิลกวดวิชา และค้นหา Stack Overflow สำหรับตัวอย่างและคำอธิบายมากมาย สเปกJSR 310

จะหาคลาส java.time ได้ที่ไหน?

  • Java SE 8 , Java SE 9และใหม่กว่า
    • ในตัว.
    • ส่วนหนึ่งของ Java API มาตรฐานพร้อมการใช้งานแบบรวม
    • Java 9 เพิ่มคุณสมบัติและการแก้ไขเล็กน้อย
  • Java SE 6และ Java SE 7
    • มากของการทำงาน java.time จะกลับรังเพลิง Java 6 และ 7 ในThreeTen-ย้ายกลับ
  • Android
    • การใช้งานคลาส java.time เวอร์ชันล่าสุดของ Android
    • สำหรับก่อนหน้านี้ของ Android ที่ThreeTenABPโครงการปรับThreeTen-ย้ายกลับ (ดังกล่าวข้างต้น) ดูวิธีใช้ ThreeTenABP … .

โครงการThreeTen-Extraขยาย java.time ด้วยคลาสเพิ่มเติม โปรเจ็กต์นี้เป็นพื้นที่พิสูจน์สำหรับการเพิ่ม java.time ในอนาคต คุณอาจพบว่าการเรียนที่มีประโยชน์บางอย่างที่นี่เช่นInterval, YearWeek, YearQuarterและอื่น ๆ อีกมากมาย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.