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