Как обрабатывать календарные часовые пояса с помощью Java? - PullRequest
91 голосов
/ 23 октября 2008

У меня есть значение Timestamp, которое приходит из моего приложения. Пользователь может находиться в любом заданном локальном часовом поясе.

Поскольку эта дата используется для веб-службы, которая предполагает, что указанное время всегда в GMT, мне нужно преобразовать параметр пользователя из скажем (EST) в (GMT). Вот кикер: пользователь не обращает внимания на его TZ. Он вводит дату создания, которую он хочет отправить на WS, поэтому мне нужно:

Пользователь вводит: 01.05.2008 18:12 (EST)
Параметр для WS должен быть : 01.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]

Ответы [ 9 ]

60 голосов
/ 23 октября 2008
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 - смещение -14400000
DEBUG - Создано время по Гринвичу с датой [Четверг, 23 08:09:05 ПО ВОСТОЧНОМУ ВРЕМЕНИ 2008]

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

Запутанная часть в том, что Calendar.getTime() возвращает вам Date в вашем текущем часовом поясе, а также что нет способа изменить часовой пояс календаря и прокрутить базовую дату. В зависимости от того, какой параметр принимает ваш веб-сервис, вы можете просто заключить сделку WS в миллисекундах с начала эпохи.

29 голосов
/ 23 октября 2008

Спасибо всем за отклик. После дальнейшего расследования я получил правильный ответ. Как упомянул Skip Head, TimeStamped, который я получал из своего приложения, настраивался на TimeZone пользователя. Таким образом, если пользователь вошел в 6:12 вечера (EST), я бы получил 2: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()));

Код выводится следующим образом: (Пользователь введен 01.05.2008 18:12 (EST)

Текущий часовой пояс пользователя: EST
Текущее смещение от времени по Гринвичу (в часах): - 4 (обычно -5, кроме случаев, когда настроено летнее время)
TS от ACP: 2008-05-01 14: 12: 00.0
Календарная дата, конвертированная из TS с использованием GMT и US_EN. Язык: 01.05.08 18:12 (GMT)

20 голосов
/ 02 февраля 2009

Вы говорите, что дата используется в связи с веб-сервисами, поэтому я предполагаю, что в некоторый момент она сериализуется в строку.

Если это так, вам следует взглянуть на метод 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());
12 голосов
/ 25 декабря 2011

Вы можете решить это с 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"));
8 голосов
/ 23 октября 2008

Похоже, что ваша метка времени настроена на часовой пояс исходной системы.

Это устарело, но должно работать:

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

Не рекомендуется использовать

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

но это должно быть сделано на стороне клиента, так как эта система знает, в каком часовом поясе она находится.

7 голосов
/ 06 апреля 2009

Способ преобразования из одной временной зоны в другую (вероятно, она работает :)).

/**
 * 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 голосов
/ 06 февраля 2012

Дата и Метка времени Объекты не учитывают часовой пояс: они представляют определенное количество секунд с начала эпохи, не принимая особую интерпретацию этого момента как часов и дней. Часовые пояса вводят изображение только в GregorianCalendar (не требуется непосредственно для этой задачи) и SimpleDateFormat , которым требуется смещение часового пояса для преобразования между отдельными полями и датой (или long ) значения.

Проблема ОП заключается в самом начале его обработки: пользователь вводит неоднозначные часы, которые интерпретируются в местном часовом поясе, отличном от GMT; в этот момент значение равно "6:12 EST" , которое можно легко распечатать как "11.12 GMT" или любой другой часовой пояс, но никогда не будет изменить до "6.12 по Гринвичу" .

Невозможно сделать SimpleDateFormat , который анализирует "06:12" как "ЧЧ: ММ" (по умолчанию используется местный часовой пояс) по умолчанию вместо UTC; SimpleDateFormat слишком умный для своего блага.

Однако вы можете убедить любой экземпляр SimpleDateFormat использовать правильный часовой пояс, если вы явно указали его во входных данных: просто добавьте фиксированную строку к полученному (и соответствующим образом проверенному) "06 : 12 " для анализа " 06:12 GMT " как " ЧЧ: ММ z ".

Нет необходимости явно задавать поля GregorianCalendar или извлекать и использовать смещения часового пояса и перехода на летнее время.

Настоящая проблема заключается в разделении входов, которые по умолчанию соответствуют местному часовому поясу, входов, которые по умолчанию имеют значение UTC, и входов, которые действительно требуют явного указания часового пояса.

4 голосов
/ 23 октября 2008

В прошлом мне удавалось определить смещение (в миллисекундах) между часовым поясом пользователя и GMT. Получив смещение, вы можете просто добавить / вычесть (в зависимости от того, в каком направлении происходит преобразование), чтобы получить подходящее время в любом часовом поясе. Обычно я выполняю это, устанавливая поле в миллисекундах объекта Calendar, но я уверен, что вы легко сможете применить его к объекту временной метки. Вот код, который я использую, чтобы получить смещение

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

timezoneId - это идентификатор часового пояса пользователя (например, EST).

1 голос
/ 26 января 2018

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

Чтобы сгенерировать строку для отображения пользователю, найдите Переполнение стека для 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 Tutorial . И поиск переполнения стека для многих примеров и объяснений. Спецификация 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
    • Более поздние версии Android связывают реализации классов java.time.
    • Для более ранних версий Android проект ThreeTenABP адаптируется ThreeTen-Backport (упоминалось выше). См. Как использовать ThreeTenABP… .

Проект ThreeTen-Extra расширяет java.time дополнительными классами. Этот проект является полигоном для возможных будущих дополнений к java.time. Здесь вы можете найти несколько полезных классов, таких как Interval, YearWeek, YearQuarter и more .

...