Как управлять часовыми поясами календаря с помощью Java?

92

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

Поскольку эта дата используется для WebService, который предполагает, что время всегда указано в 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]

Хорхе Валуа
источник
2
Почему вы меняете только часовой пояс, а не конвертируете дату / время вместе с ним?
Спенсер Кормос,
1
Это настоящая боль - взять дату Java, которая находится в одном часовом поясе, и получить эту дату в другом часовом поясе. IE, возьмите 17:00 EDT и получите 5PM 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 - смещение составляет -14400000
DEBUG - создана календари по Гринвичу с датой [Thu Oct 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 01
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()));

Вывод кода: (Пользователь ввел 01.05.2008 18:12 (EST)

Часовой
пояс текущего пользователя: EST Текущее смещение от GMT (в часах): - 4 (Обычно -5, кроме корректировки DST)
TS из ACP: 2008-05-01 14: 12: 00.0
Календарная дата преобразована из TS с использованием локали GMT и US_EN : 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. Если бы был собственный часовой пояс, удивлялся, почему изменение часового пояса в календаре, кажется, не имеет никакого эффекта!
Chris.Jenkins
TimeZone.getTimeZone ("UTC") недействителен, так как UTC отсутствует в AvailableIDs () ... бог знает почему
childno͡.de
TimeZone.getTimeZone ("UTC") доступен на моей машине. Это зависит от JVM?
CodeClimber
2
Замечательная идея с методом setTimeZone (). Вы избавили меня от головной боли, большое спасибо!
Bogdan Zurac 01
1
Как раз то, что мне было нужно! Вы можете установить часовой пояс сервера в программе форматирования, и тогда, когда вы конвертируете его в формат календаря, вам не о чем будет беспокоиться. Безусловно, лучший метод! для getTimeZone вам может потребоваться использовать формат Ex: "GMT-4: 00" для ETD
Майкл Керн,
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, который напрямую несовместим без побочных зависимостей и дополнительной конфигурации. Однако для третьей версии это был самый быстрый и простой в использовании подход.
Баклажан
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;
}
Helpa
источник
6

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

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

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

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

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

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

user412090
источник
4

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

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

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

Адам
источник
9
Используя необработанное смещение, вы игнорируете DST.
Вернер Леманн,
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со смещением , используя константу для 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 . И поищите в 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, и более .

Василий Бурк
источник