Поведение Java ZonedDateTime.toInstant () - PullRequest
0 голосов
/ 08 декабря 2018

Я использую следующие выражения: 7 декабря 2018 .

Я вижу расхождение, из-за которого:

ZonedDateTime.now(ZoneId.of("America/New_York")).minusDays(30)

возвращает (правильно):

2018-11-07T22:44:11.242576-05:00[America/New_York]

, тогда как преобразование в мгновение:

ZonedDateTime.now(ZoneId.of("America/New_York")).minusDays(30).toInstant()

, кажется, испортило результат , добавив к нему дополнительный день :

2018-11-08T03:58:01.724728Z

Мне нужно мгновенное преобразование, чтобы использовать его результат в следующем коде в качестве даты:

... = Date.from(t.toInstant()) 

Эквивалентный код Python (Django) работает правильно:

datetime.datetime.now('America/New_York')+datetime.timedelta(days=-30)

, вычисляя в: datetime: 2018-11-07 20:13:55.063888-05:00

Что вызывает несоответствие?

Что я должен использовать, чтобы преобразование Java в Date привело к возвращению 7 ноября , как в случае с Python?По сути, я ищу эквивалентный перевод этого кода Python на Java или в псевдокоде:

`datetime.X = datetime.now(deployment_zone) - (N_days)`,

where `deployment_zone` is configurable (i.e. `America/New_York`)

`N_days` is configurable (i.e. 30)

Обновление для @Basil Bourque:

Когда я сформулировал исходный вопрос, я (согласно правилам SO) попытался упростить его до удобоваримой формы, которая, вероятно, разрушила большую часть необходимого контекста, сделав его расплывчатым.Позвольте мне повторить попытку.

Как я объяснил в комментариях, я преобразую существующий код Python (который поддерживается более активно и который клиент хочет сохранить нетронутым) в существующий код Java (устаревший, который не былдолжным образом поддержанный и отклоненный от логики Питона некоторое время назад).Обе базы кода должны быть функционально на одном уровне друг с другом.Java должна делать то, что Python уже делает.

Код Python выглядит следующим образом (для краткости я собираю все в одно место, в действительности он распределяется по паре файлов):

analytics.time_zone=America/New_York
TIME_ZONE = props.getProperty('analytics.time_zone', 'UTC')
TZ = pytz.timezone(TIME_ZONE)

    def days_back(num_days=0):
            adjusted_datetime = datetime.datetime.now(TZ)+datetime.timedelta(days=-num_days) 
            return DateRangeUtil.get_start_of_day(adjusted_datetime)

class DateRangeUtil():

    @staticmethod
    def get_start_of_day(date):
        return date.astimezone(TZ).replace(hour=0, minute=0, second=0, microsecond=0)

, который в основном берет настроенный часовой пояс, в котором он получает текущий момент, вычитает из него указанное количество дней, преобразует его в начало этой даты и, таким образом, получает нижнюю границудиапазон, используемый при запросе к БД, что-то вроде времени начала: datetime: 2018-11-07 20:13:55.063888-05:00

Когда я начал на стороне Java , он имел:

    public final static DateRange parse(String dateRange) {
                //.....
                        int days = ...
                        return new DateRange(toBeginningOfDay(daysBack(days)), toEndOfDay(daysBack(0)));

    private final static Date daysBack(int days) {
                return toDate(LocalDateTime.now().minusDays(days));
            }

private final static Date toBeginningOfDay(Date d)
        {
            Calendar c=Calendar.getInstance();
            c.setTime(d);
            c.set(HOUR_OF_DAY,0);
            c.set(MINUTE,0);
            c.set(SECOND,0);
            c.set(MILLISECOND, 0);
            return c.getTime();
        }

        private final static Date toDate(LocalDateTime t) {
            return Date.from(t.atZone(ZoneId.systemDefault()).toInstant());
        }

Этот кодне сработал и ввел несоответствие , которое я описал в своем первоначальном вопросе.Я начал экспериментировать и ввел ZonedDateTime на картинку.В ходе расследования я обнаружил, что это вызов .toInstant (), который кажется виновником, и хотел более подробно понять, что за этим стоит.

В своем ответе @ernest_k предложилрешение, которое, казалось, сработало, но я все еще не совсем понял, что ясно из вопросов в комментариях к его ответу.

Изменения, которые я сделал на основе ответа @ernest_k, следующие:

private final static Date daysBack(int days) {

            return toDate(ZonedDateTime.now(ZoneId.of("America/New_York")).minusDays(days).toLocalDateTime());

private final static Date toDate(LocalDateTime t) {
            return Date.from(t.toInstant(ZoneOffset.UTC));
        }

Похоже, что это дает желаемый результат: однако преобразование из локального в зонированное и затем обратно выглядело слишком много, поэтому я немного поэкспериментировал и обнаружил, что просто LocalDateTime тоже помогает:

private final static Date toDate(LocalDateTime t) {
            return Date.from(t.toInstant(ZoneOffset.UTC));
        }

private final static Date daysBack(int days) {
            return toDate(LocalDateTime.now().minusDays(days));
        }

Я вижу, что LocalDate (и, возможно, LocalDateTime) имеет удобный atStartOfDay(), который, кажется, является подходящим кандидатом для исключения Date из картинки при замене устаревшего toBeginningOfDay(Date d)метод выше.Не уверен, что он делает то же самое - я еще не экспериментировал с этой идеей, поэтому предложения приветствуются.

Итак, со всеми вышеперечисленными проблемами, мой вопрос начался с поведения toInstant(), икогда ему передается идентификатор зоны, преобразует ли он в мгновение в этой зоне, или ОТ этого, или что ?

Я предполагаю, что для ситуации, которую я описываю, мы заботимся только о том, чтобы нижняя временная граница в запросе к БД формировалась путем сравнения некоторого последовательного маркера текущего времени (его верхней границы) с тем, чтоон был в том же месте (часовой пояс?) в последние N дней, поэтому сравнение его с UTC должно служить цели.

Значит ли это, что пропуск зоны становится ненужным?

Теперь, когда кажется, что решение найдено, вопрос вращается вокруг обоснованности подхода, описанного выше, и решения, на которое мы наткнулись - является ли он наиболее оптимальным, лучшие практики для библиотек синхронизации Java и т. Д.код должен работать для любого часового пояса, в котором базы кода будут развернуты, поэтому зона передается через конфигурацию.

Кроме того, мне интересно, изменится ли ситуация, когда / если сама БД развернута вне помещения из остальной базы кода и настроена для сохранения данных в каком-либо другом часовом поясе.Но это может быть другой вопрос.* * 1097

Ответы [ 3 ]

0 голосов
/ 08 декабря 2018

Во-первых, избегайте необходимости старомодного Date, если можете.java.time, современный Java-интерфейс даты и времени, предоставляет вам все необходимые функции.

Иногда нам требуется Date для устаревшего API, который мы не можем изменить или не хотим просто обновлять.сейчас.Java дает вам то, что, я думаю, вы хотите.Демонстрация:

    ZonedDateTime nov7 = ZonedDateTime.of(2018, 11, 7, 22, 44, 0, 0,
            ZoneId.of("America/New_York"));
    Instant inst = nov7.toInstant();
    System.out.println("As Instant: " + inst);
    Date oldFashionedDate = Date.from(inst);
    System.out.println("As Date: " + oldFashionedDate);

Вывод из этого был:

As Instant: 2018-11-08T03:44:00Z
As Date: Wed Nov 07 22:44:00 EST 2018

Допустим, чтобы получить этот вывод, мне пришлось изменить часовой пояс по умолчанию для JVM на America / New_Yorkfirst.

Date и Instant примерно эквивалентны, но печатаются по-разному.То есть их toString методы ведут себя по-разному, что может сбивать с толку.Каждый является моментом времени, ни один из них не является датой (несмотря на название одного из них).Это не всегда одна и та же дата во всех часовых поясах.

Date.toString выбирает настройку часового пояса вашей JVM и использует ее для генерации возвращаемой строки.Instant.toString с другой стороны всегда использует UTC для этой цели.Вот почему один и тот же момент времени печатается с разными датой и временем.К счастью, они оба также печатают немного информации о часовом поясе, поэтому разница по крайней мере заметна.Date печатает EST, что, хотя и неоднозначно, в данном случае означает восточное стандартное время.Instant печатает Z для смещения нуля от UTC или «времени Зулуса».

0 голосов
/ 08 декабря 2018

tl; dr

ZonedDateTime.toInstant() настраивает момент от часового пояса до UTC.Вы получите один и тот же момент, другое время на настенных часах и , возможно, другую дату для одной и той же точки на временной шкале.То, что вы видите, не является проблемой, не является несоответствием .

Ваша проблема не в вычитании 30 дней.Реальные проблемы:

  • Непонимание того, что часовой пояс влияет на дату
  • Сопоставление дат с днями

Кроме того, ваш вопрос неопределенный.Сказать «30 дней назад» может означать как минимум три разные вещи:

  • 30 * 24 часа
  • Диапазон от 22:44 тридцать календарных дней назад в часовом поясе Нью-Йорка до 22: 44 сейчас по нью-йоркскому времени
  • Весь сегодняшний день, как это видно в Нью-Йорке, и целые дни, возвращающиеся на 30 дней в календаре, как в Нью-Йорке.

Все три возможности описаны ниже, с примером кода, помеченного .

⑦? ??? ↔ ??? ⑧?

7 декабря, незадолго до полуночи (22:44), Алиса в своей нью-йоркской квартире решает позвонить своему другу Бобув Рейкьявике, Исландия.Боб не может поверить, что его телефон звонит, и, глядя на часы на тумбочке, видит, что почти 4 часа утра (03:44).И модные цифровые часы Боба показывают дату 8-го декабря, а не 7-го.Тот же самый момент, та же точка на временной шкале, другое время на настенных часах, другая дата .

Люди Исландии используют UTC в качестве своего часового пояса круглый год.Нью-Йорк на пять часов отстает от UTC в декабре 2018 года, и поэтому на пять часов отстает от Исландии.В Нью-Йорке «вчера» 7-е, а в Исландии «завтра» 8-е.Разные даты, один и тот же момент.

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

Нет расхождений, дополнительный день не добавляется. В любой момент времени дата меняется по всему земному шару в зависимости от часового пояса.С диапазоном часовых поясов около 26-27 часов, это всегда где-то «завтра» и «вчера».

Другой ответ предлагает включить LocalDateTime в эту проблему.Это опрометчиво.В этом классе намеренно отсутствует понятие часового пояса или смещения от UTC.Это означает, что LocalDateTime не может представлять момент.LocalDateTime представляет потенциальных моментов в диапазоне 26-27 часов, упомянутых выше.Не имеет смысла использовать этот класс здесь.

Вместо этого используйте OffsetDateTime для момента просмотра со смещением от UTC, по сравнению с [ZonedDateTime][2], в котором используется часовой пояс.

В чем разница между смещением и зоной?Смещение - это просто количество часов-минут-секунд, ни больше, ни меньше.Зона, напротив, на намного * на 1065 * больше.Зона - это история прошлых, настоящих и будущих изменений в смещении, используемом людьми определенного региона.Поэтому часовой пояс всегда предпочтительнее простого смещения, так как он приносит больше информации.Если вы хотите использовать UTC, вам нужно только смещение, ноль часов, минут и секунд.

OffsetDateTime odt = zdt.toOffsetDateTime().withOffsetSameInstant( ZoneOffset.UTC ) ;  // Adjust from a time zone to UTC. 

Видимые здесь zdt и odt представляют один и тот же момент, одну и ту же точку на временной шкале, разное время на настенных часах, как в примере Алисы и Боба выше.

Дни! = Даты

Если вы хотите сделать запрос для диапазона тридцати дней назад, вы должны определить, что вы подразумеваете под «днями».

Дней

you Вы имеете в виду 30 кусков продолжительностью 24 часа?Если это так, работайте с Instant.Этот класс представляет момент в UTC, всегда в UTC.

ZoneId z = ZoneId.of( "America/New_York" ) ;
ZonedDateTime zdtNow = ZonedDateTime.now( z ) ;
Instant instantNow = zdt.toInstant() ;  // Adjust from time zone to UTC. Same moment, different wall-clock time.
Instant instantThirtyDaysAgo = instantNow.minus( 30 , ChronoUnit.DAYS ) ; // Subtract ( 30 * 24 hours ) without regard for dates. 

Вы можете иметь возможность обмениваться Instant с вашей базой данных через драйвер JDBC.Но Instant является необязательным, в то время как поддержка OffsetDateTime требуется для JDBC 4.2 и более поздних версий.Если это так, давайте перепишем этот код.

ZoneId z = ZoneId.of( "America/New_York" ) ;
ZonedDateTime zdtNow = ZonedDateTime.now( z ) ;
OffsetDateTime odtNow = zdt.toOffsetDateTime().withOffsetSameInstant( ZoneOffset.UTC ) ;  // Adjust from time zone to UTC. Same moment, different wall-clock time.
OffsetDateTime odtThirtyDaysAgo = odtNow.minusDays( 30 ) ;

Ваш SQL может выглядеть примерно так:

Обратите внимание, что мы используем подход Half-Open для определения промежутка времени, где начало включительно , а окончание исключительно .Как правило, это лучшая практика, так как она позволяет избежать проблемы нахождения бесконечно делимого последнего момента и обеспечивает аккуратные стыковки без разрывов.Поэтому мы не используем команду SQL BETWEEN, будучи полностью закрытыми (включительно с обоих концов).

SELECT * FROM event_ WHERE when_ >= ? AND when_ < ? ;

Установите значения для заполнителей в подготовленном выражении.

myPreparedStatement.setObject( 1 , odtThirtyDaysAgo ) ;
myPreparedStatement.setObject( 2 , odtNow ) ;

Даты

➥ Если под «30 дней назад» вы имели в виду 30 коробок на календаре, висящем на стене в нью-йоркском офисе, это совсем другая проблема.

В то же время суток

И если да, то имеете ли вы в виду текущий момент и переход назад на 30 дней к тому же времени дня?

ZoneId z = ZoneId.of( "America/New_York" ) ;
ZonedDateTime zdtNow = ZonedDateTime.now( z ) ;
ZonedDateTime zdtThirtyDaysAgo = zdtNow.minusDays( 30 ) ; // `ZonedDateTime` will try to keep the same time-of-day but will adjust if that time on that date in that zone is not valid.

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

Перейдите к подготовленному заявлению.

myPreparedStatement.setObject( 1 , zdtThirtyDaysAgo ) ;
myPreparedStatement.setObject( 2 , zdtNow ) ;

Весь день

➥Или под «30 дней назад» вы подразумеваете даты, а под датами вы подразумеваете весь день?

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

ZoneId z = ZoneId.of( "America/New_York" ) ;
LocalDate today = LocalDate.now( z ) ;
LocalDate tomorrow = today.plusDays( 1 ) ;
LocalDate thirtyDaysAgo = tomorrow.minusDays( 30 ) ;

Теперь нам нужно перейти от даты к определенному моменту, назначив время дня и часовой пояс.Мы хотим, чтобы время было первым моментом дня.Не думайте, что это означает 00:00.Из-за аномалий, таких как DST, день может начаться в другое время, например 01:00.Пусть java.time определит первый момент дня в эту дату в этой зоне.

ZonedDateTime zdtStart = thirtyDaysAgo.atStartOfDay( z ) ;
ZonedDateTime zdtStop = tomorrow.atStartOfDay( z ) ;

Перейдите к подготовленному заявлению.

myPreparedStatement.setObject( 1 , zdtStart ) ;
myPreparedStatement.setObject( 2 , zdtStop ) ;
0 голосов
/ 08 декабря 2018

Этот «дополнительный день» на самом деле не дополнительный день.2018-11-07T22:44:11 в Нью-Йорке эквивалентно 2018-11-08T03:58:01 в UTC (это тот же момент времени).Разница составляет всего 5 часа, а не дня (и когда я гуглю это, я вижу, что Нью-Йорк GMT-5).

ZonedDateTime#toInstant возвращает экземпляр Instant, представляющий тот же момент времени(в UTC):

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

Если вы хотите, чтобы не использовало смещение при конвертации в мгновенное значение, тогда вам, возможно, следует использовать LocalDateTime:

ZonedDateTime.now(ZoneId.of("America/New_York"))
      .toLocalDateTime()
      .toInstant(ZoneOffset.UTC) 

Это говорит о необходимости преобразования, как если бы это было уже UTC время (но здесь уместно предупреждение: это меняет значение даты / времени )

...