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


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

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

пользователь вводит: 5/1/2008 6: 12 PM (EST)
параметр для WS должен быть: 5/1/2008 6: 12 PM (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 11:12 PM]

9 88

9 ответов:

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

DEBUG - input календарь имеет дату [Чт 23 октября 12: 09: 05 EDT 2008]
DEBUG-offset -14400000
Отладка-создан GMT cal с датой [Чт 23 октября 08: 09: 05 EDT 2008]

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

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

спасибо всем за ответы. После дальнейшего расследования я получил правильный ответ. Как упоминалось Skip Head, отметка времени, которую я получал из своего приложения, была настроена на часовой пояс пользователя. Поэтому, если пользователь ввел 6:12 PM (EST), я бы получил 2:12 PM (GMT). Мне нужен был способ отменить преобразование, чтобы время, введенное пользователем, было временем, которое я отправил на запрос веб-сервера. Вот как я это сделал:

// 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 6: 12PM (EST)

часовой пояс текущего пользователя: EST
Текущее смещение от GMT (в часах): -4 (обычно -5, за исключением DST регулируется)
TS от ACP: 2008-05-01 14: 12: 00.0
Календарная дата, преобразованная из TS с помощью GMT и Us_en Locale: 5/1/08 6: 12 PM (GMT)

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

Если это так, вы должны взглянуть на метод 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());

Вы можете решить ее с Джода Времени:

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

способ преобразования данных из одного часового пояса в другой(вероятно, это работает :) ).

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

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

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

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

не устарел способ использовать

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

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

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

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

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

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

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

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

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

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

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

java.время

современный подход использует java.время классы, которые вытеснили проблемные устаревшие классы даты-времени в комплекте с самыми ранними версиями Java.

The java.sql.Timestamp класс является одним из тех устаревших классов. Больше не нуждаться. Вместо этого используйте Instant или другой java.классы времени непосредственно с вашей базой данных с помощью JDBC 4.2 и более поздних версий.

The Instant класс представляет момент на временной шкале в UTC с разрешением наносекунд (до девяти (9) цифр десятичной дробью).

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

если вы должны взаимодействовать с существующей Timestamp, немедленно конвертировать в java.время с помощью новых методов преобразования, добавленных к старым классам.

Instant instant = myTimestamp.toInstant() ;

чтобы перейти в другой часовой пояс, укажите часовой пояс как , теперь режим обслуживания, советует миграция в java.время классы.

чтобы узнать больше, смотрите Oracle Tutorial. Поиск переполнения стека для многих примеров и объяснений. Спецификация является JSR 310.

где получить java.время занятий?

  • 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 bundle реализации java.время занятия.
    • для более ранних Android, ThreeTenABP проект приспосабливается ThreeTen-Backport (упоминалось выше). Смотрите как использовать ThreeTenABP....

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