Як обробляти часові пояси календаря за допомогою Java?


92

У мене є значення Timestamp, яке походить від моєї програми. Користувач може знаходитись у будь-якому місцевому часовому поясі.

Оскільки ця дата використовується для WebService, яка передбачає, що заданий час завжди в GMT, мені потрібно перетворити параметр користувача з скажімо (EST) на (GMT). Ось кікер: користувач не звертає уваги на свій TZ. Він вводить дату створення, яку хоче надіслати до WS, тож мені потрібно:

Користувач вводить: 01.05.2008 18:12 (EST)
Параметр для WS повинен бути : 05/05/2008 18:12 (GMT)

Я знаю, що мітки часу за замовчуванням завжди повинні бути в 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 травня 2008 р., 23:12]


2
Чому ви лише змінюєте часовий пояс, а не перетворюєте дату / час разом з ним?
Спенсер Кормос

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()) у:

ДЕБУГ - вхідний календар має дату [чт. 23 жовтня, 12:09:05, EDT 2008] ДЕБУГ
- зсув становить -14400000 ДЕБУГ
- створений GMT-кал з датою [чт, 23 жовтня, 08:09:05, EDT, 2008]

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

Збиває з пантелику те, що Calendar.getTime()повертає вам a Dateу вашому поточному часовому поясі, а також те, що не існує способу зміни часового поясу календаря, а також прокручування базової дати. Залежно від того, який тип параметрів приймає ваша веб-служба, ви можете просто захотіти отримати WS-угоду в термінах мілісекунд від епохи.


11
Чи не слід вам віднімати, offsetFromUTCа не додавати? На вашому прикладі, якщо 12:09 GMT становить 8:09 EDT (що відповідає дійсності), а користувач вводить "12:09 EDT", алгоритм повинен видавати "16:09 GMT", на мій погляд.
DzinX

29

Дякую всім за відповідь. Після подальшого розслідування я дійшов правильної відповіді. Як згадував Skip Head, TimeStamped, який я отримував від моєї програми, підлаштовувався під часовий пояс користувача. Отже, якщо Користувач увійшов 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()));

Вихідні дані коду: (Користувач введений 05.08.2008 18:12 (EST)


Часовий пояс поточного користувача: EST Поточний зсув від GMT (у годинах): - 4 (Зазвичай -5, за винятком налаштування літнього часу)
TS від ACP: 2008-05-01 14: 12: 00
Дата календаря перетворена з TS за допомогою GMT та US_EN Locale : 01.05.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.getTimeZone ("UTC") недійсний, оскільки UTC відсутній у AvailableIDs () ... Бог знає чому
childno͡.de

TimeZone.getTimeZone ("UTC") доступний на моєму комп'ютері. Це залежить від JVM?
CodeClimber

2
Чудова ідея з методом setTimeZone (). Ви врятували мені багато головного болю, дякую!
Богдан Зурак 01.03.13

1
Якраз те, що мені потрібно! Ви можете встановити часовий пояс сервера у форматері, і тоді, коли ви перетворюєте його у формат календаря, вам не про що турбуватися. На сьогодні найкращий метод! для getTimeZone вам може знадобитися використовувати формат Ex: "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"));

однак подбайте про це, якщо ви використовуєте hibernate 4, який не є безпосередньо сумісним без побічної залежності та додаткової конфігурації. Однак це був найшвидший і найпростіший у використанні підхід для третьої версії.
Баклажани

8

Схоже, для вашої часової позначки встановлено часовий пояс системи відправлення.

Це застаріло, але воно має працювати:

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 , для змінення часового поясу необхідне зміщення між окремими полями та значеннями дати (або довгими ).

Проблема ОП знаходиться на самому початку його обробки: користувач вводить години, які є неоднозначними, і вони інтерпретуються в місцевому часовому поясі, що не за Гринвічем; на даний момент значення дорівнює "6: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. Після того, як ви отримаєте зсув, ви можете просто додати / відняти (залежно від того, яким шляхом відбувається перетворення), щоб отримати відповідний час в будь-якому часовому поясі. Зазвичай я досягав би цього, встановлюючи поле мілісекунд для об’єкта Календаря, але я впевнений, що ви могли б легко застосувати його до об’єкта часової мітки. Ось код, який я використовую для компенсації

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

timezoneId - це ідентифікатор часового поясу користувача (наприклад, EST).


9
Використовуючи вихідне зміщення, ви ігноруєте літній час.
Вернер Леманн

1
Точно, цей метод дасть неправильні результати протягом півроку. Найгірша форма помилки.
Дунайський моряк

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зі зміщенням , використовуючи константу для UTC, ZoneOffset.UTC.

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, Calendar, іSimpleDateFormat .

Проект Joda-Time , який зараз перебуває в режимі обслуговування , радить перейти на класи java.time .

Щоб дізнатись більше, див. Підручник Oracle . І шукайте в 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-Backport .
  • Android

Проект ThreeTen-Extra розширює java.time додатковими класами. Цей проект є полігоном для можливих майбутніх доповнень до java.time. Ви можете знайти деякі корисні класи тут , такі як Interval, YearWeek, YearQuarter, і більш .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.