DateUtils#truncate() для работы в часовом поясе UTC

У меня есть java.util.Date, на котором я использую org.apache.commons.lang.time#truncate(javaUtilDateObj, Calendar.DATE) для преобразования даты в полночь.

Проблема в том, что дата UTC представляется в местном часовом поясе, а метод усечения просто преобразует часы, минуты и секунды в 00:00:00 без учета часового пояса.

Пример: - Скажем, время UTC составляет 30 секунд от эпохи. Ср 31 декабря 19:00:30 ОЦЕНКА 1969 - это дата объекта, который я вижу. При вызове метода DateUtils#truncate() в указанную выше дату выводится в среду, 31 декабря, 00:00:00 EST 1969.

То, что я ожидаю, это если время UTC составляет 30 секунд от эпохи. И если это можно представить / преобразовать в четверг 1 января 00:00:30 по восточному времени 1970 года, я могу вызвать для этого метод DateUtils#truncate() и ожидать, что 1 января 00:00:00 по восточному времени 1970 года.

Примечание: я не в состоянии использовать joda-time API и, следовательно, я застрял с тем, что у меня есть.

4 ответа

Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
utc.setTime(date);
Calendar midnight = DateUtils.truncate(utc, Calendar.HOUR_OF_DAY);
Date midnightDate = midnight.getTime();

Следующая функция полезна, если ваш уровень данных сохраняет время UTC, но у вашего уровня представления есть другая временная зона, и вам нужно усечь дату для фильтрации.

/**
 * Truncates the given UTC date for the given TimeZone
 *
 * @param date UTC date
 * @param timeZone Target timezone
 * @param field Calendar field
 *
 * @return
 */
public static Date truncate(Date date, final TimeZone timeZone, final int field)
{
    int timeZoneOffset = timeZone.getOffset(date.getTime());

    // convert UTC date to target timeZone
    date = DateUtils.addToDate(date, Calendar.MILLISECOND, timeZoneOffset);

    // truncate in target TimeZone
    date = org.apache.commons.lang3.time.DateUtils.truncate(date, field);

    // convert back to UTC
    date = DateUtils.addToDate(date, Calendar.MILLISECOND, -timeZoneOffset);

    return date;
}

/**
 * Adds the given amount to the given Calendar field to the given date.
 *
 * @see Calendar
 *
 * @param date the date
 * @param field the calendar field to add to
 * @param amount the amount to add, may be negative
 *
 * @return
 */
public static Date addToDate(final Date date, final int field, final int amount)
{
    Objects.requireNonNull(date, "date must not be null");

    final Calendar c = Calendar.getInstance();
    c.setTime(date);
    c.add(field, amount);
    return c.getTime();
}

ТЛ; др

  • Используйте современные классы java.time. Встроенный в Java.
  • День может не начинаться в 00:00:00 в некоторые даты в некоторых местах.
    • Пусть java.time определит первый момент дня.
  • Избегайте двусмысленного термина "полночь".
    • Подумайте о "первом моменте дня".
    • Поиск переполнения стека, чтобы узнать о полуоткрытых промежутках времени.

Первый момент дня:

ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;    // Specify the region whose wall-clock time you want to perceive “today”. Specify time zone with `Continent/Region`, never the 3-4 letter pseudo-zones such as `EST` or `IST`. 
LocalDate today = LocalDate.now( z ) ;          // Capture the current date as seen in a specific time zone.
ZonedDateTime zdt = today.atStartOfDay( z ) ;   // Let java.time determine first moment of the day.
Instant instant = zdt.toInstant() ;             // Adjust from time zone to UTC. Same moment, same point on the timeline, different wall-clock time.

подробности

Здесь много проблем.

Регулировка зон

если время UTC составляет 30 секунд от эпохи. И если это может быть представлено / преобразовано в Чт 1 января 00:00:30 EST 1970,

Без ошибок. Вы не можете конвертировать. Эти два значения даты и времени не представляют один и тот же момент. Если по EST Вы имели в виду часовой пояс, такой как America/New_York или же America/Montreal, тогда эти два значения разнесены на несколько часов.

Instant thirtySecondsFromEpoch = Instant.EPOCH.plusSeconds( 30 ) ;

тридцатисекунды FromEpoch.toString(): 1970-01-01T00:00:30Z

Посмотрите тот же момент на большей части восточного побережья Северной Америки.

ZoneId z = ZoneId.of( "America/New_York" ) ;
ZonedDateTime zdtAdjustedFromThirtySecondsFromEpoch = thirtySecondsFromEpoch.atZone( z ) ;

zdtAdjustedFromThirtySecondsFromEpoch.toString (): 1969-12-31T19: 00: 30-05: 00 [America / New_York]

Поскольку большинство людей на восточном побережье Северной Америки используют время настенных часов, которое на пять часов отстает от UTC, тот же самый момент в 30 секунд после начала эпохи видится как полминуты после 7 вечера за день до, последнего дня 1969.

Попробуйте то же самое время суток, 30 секунд в первый день 1970 года, но, как видно на America/New_York часовой пояс, а не в UTC. Теперь мы говорим о совершенно другом моменте. Полминуты в новый день происходит на пять часов позже, чем полминуты нового дня в UTC.

ZonedDateTime zdt = ZonedDateTime.of( 1970 , 1 , 1 , 0 , 0 , 30 , 0 , z ) ;

zdt.toString (): 1970-01-01T00: 00: 30-05: 00 [America / New_York]

zdt.toInstant (). toString (): 1970-01-01T05: 00: 30Z

Часовой пояс

Вы игнорируете важнейшую проблему часового пояса.

Часовой пояс имеет решающее значение при определении даты. В любой момент времени дата меняется по всему земному шару в зависимости от зоны. Например, через несколько минут после полуночи в Париже Франция - новый день, а в Монреале-Квебеке все еще "вчера".

Укажите правильное название часового пояса в формате continent/region, такие как America/Montreal, Africa/Casablanca, или же Pacific/Auckland, Никогда не используйте 3-4 буквенное сокращение, такое как EST или же IST поскольку они не являются истинными часовыми поясами, не стандартизированы и даже не уникальны (!).

java.util.Date::toString

toString метод на Date имеет благонамеренное, но проблемное поведение - неявно применять текущий часовой пояс JVM по умолчанию при генерации строки, представляющей Date UTC значение объекта. Создает ложное впечатление, что эта зона присутствует в Date,

Это одна из многих причин, чтобы избежать этого ужасного класса. Вместо этого используйте классы java.time.

java.time

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

Если у тебя есть Date в руки, конвертировать в / из java.time.Instant через новые методы, добавленные к старому классу.

Instant instant = myJavaUtilDate.toInstant() ;

Старайтесь не говорить о "полуночи", поскольку это аморфная тема. Вместо этого сосредоточьтесь на первом моменте дня, начале дня.

Не думайте, что день начинается в 00:00:00. Аномалии, такие как переход на летнее время (DST), означают, что день может начинаться в другое время суток, например, 01:00:00. Пусть java.time определит начало дня.

Определение начала дня означает определение даты. А для определения даты требуется часовой пояс, как упоминалось выше. Так что нам нужно отрегулировать от вашего Instant в UTC к ZonedDateTime в определенном часовом поясе (ZoneId)

ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;

Выписка из этого ZonedDateTime только часть только для даты.

LocalDate ld = zdt.toLocalDate() ;

Теперь спросите о первом моменте дня в указанном часовом поясе.

ZonedDateTime zdtStartOfDay = ld.atStartOfDay( z ) ;

Усечение

Вы можете обрезать различные объекты java.time. Искать truncatedTo метод, где вы передаете TemporalUnit объект (вероятно, ChronoUnit), чтобы указать гранулярность, которую вы хотите в своем результате. Нет необходимости в Apache DateUtils для этой цели.

Если усечь Instant Вы всегда используете UTC для логики часового пояса.

Instant instantTrucToDay = Instant.now().truncatedTo( ChronoUnit.DAYS ) ;

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

ZonedDateTime zdtTruncToDay = zdt.truncatedTo( ChronoUnit.DAYS ) ;

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

если время UTC составляет 30 секунд от эпохи. И если это может быть представлено / преобразовано в Чт 1 января 00:00:30 EST 1970

Не уверен, что ты имел в виду здесь. Но имейте в виду, что через 30 секунд после эпохи 1970-01-01T00:00:00Z (1970-01-01T00:00:30Z) тот же самый момент на восточном побережье Северной Америки происходит на несколько часов раньше. Это означает, что около 7 вечера предыдущего дня.

Instant instantThirtySecsAfterEpoch = Instant.EPOCH.plusSeconds( 30 ) ;

instantThirtySecsAfterEpoch.toString (): 1970-01-01T00: 00: 30Z

ZoneId z = ZoneId.of( "America/New_York" ) ;
ZonedDateTime zdt = instantThirtySecsAfterEpoch.atZone( z ) ;

zdt.toString (): 1969-12-31T19: 00: 30-05: 00 [Америка / Нью-Йорк]


О java.time

Инфраструктура java.time встроена в Java 8 и более поздние версии. Эти классы вытесняют проблемные старые классы даты и времени, такие как java.util.Date, Calendar & SimpleDateFormat,

Проект Joda-Time, находящийся сейчас в режиме обслуживания, рекомендует перейти на классы java.time.

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

Используя драйвер JDBC, соответствующий JDBC 4.2 или более поздней версии, вы можете обмениваться объектами java.time напрямую с вашей базой данных. Нет необходимости в строках или классах java.sql.*.

Где взять классы 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 и многое другое.

Использовать Calendar версия truncate:

Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")).setTime(date);
Calendar midnight = DateUtils.truncate(utc, Calendar.HOUR_OF_DAY);
Date midnightDate = midnight.getTime();
Другие вопросы по тегам