Согласованный часовой пояс для сравнения дат

Я хочу передать строку даты и проверить, предшествует ли эта дата текущему времени MST.

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

На местном уровне я нахожусь в Великобритании. И когда я его развертываю, сервер находится в Фениксе.

Сейчас в Великобритании время 2024-06-28 15.45.00, и эта логика выдает true для isValidDate в настоящее время, когда я ввожу 2024-06-28 15.00.00.

Но я устанавливаю зону MST. Я ожидал, что это ложь.
МСК сейчас около 8 утра. Так что не раньше. Кажется, оно продолжает работать против британского времени.

Как я могу обновить это, чтобы при развертывании он проверял строку даты по времени MST. И локально продолжать тоже баллотироваться за МСТ? По сути, если я окажусь на другом сервере в Австралии, логика должна продолжать работать по времени MST.

private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH.mm.ss");
private static final ZoneId MST_TIMEZONE = ZoneId.of("America/Phoenix");

// Spring bean set to Clock.systemDefaultZone(); Can't change this.
// using a clock for unit testing purposes.
private final Clock clock;

private ZonedDateTime parseDate(String dateStr) {
    try {
        return LocalDateTime
                .parse(dateStr, DATE_FORMATTER)
                .atZone(MST_TIMEZONE);
    } catch (DateTimeParseException e) {
        return null;
    }
}

private boolean isValidDate(String startDateTime) {
    ZonedDateTime start = parseDate(startDateTime);        
    return start != null
            && start.isBefore(LocalDateTime.now(clock).atZone(MST_TIMEZONE));
}

Могу ли я возразить против вашего именования: у вас есть parseDate, возвращающий ZonedDateTime; у вас есть isValidDate принятие значения под названием *DateTime. Вам следует выбирать имена, которые точно описывают то, что вы делаете, особенно в такой путанице, как даты и время.

Andy Turner 28.06.2024 18:01

И пока мы говорим об именовании... часовой пояс в Финиксе сейчас летом - MDT, а не MST.

k314159 28.06.2024 19:13

Общее правило работы с датами без безумия: внутри вашего кода и базы данных (если она есть) храните все в формате UTC. Преобразуйте из местного значения в формат UTC на входе и из формата UTC в локальный на выходе. Класс java.time.Instant — ваш друг.

Jim Garrison 29.06.2024 02:32
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
5
3
115
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

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

Вот обновленная версия вашего кода, которая гарантирует, что строка даты анализируется и правильно сравнивается со временем MST, независимо от того, где она выполняется:

 private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH.mm.ss");
 private static final ZoneId MST_TIMEZONE = ZoneId.of("America/Phoenix");

 private final Clock clock;

 public DateValidator(Clock clock) {
     this.clock = clock;
 }

 private ZonedDateTime parseDate(String dateStr) {
     try {
         return LocalDateTime
                 .parse(dateStr, DATE_FORMATTER)
                 .atZone(MST_TIMEZONE);
     } catch (DateTimeParseException e) {
         return null;
     }
 }

 public boolean isValidDate(String startDateTime) {
     ZonedDateTime start = parseDate(startDateTime);
     if (start == null) {
         return false;
     }

     // Get the current time in MST
     ZonedDateTime nowInMST = ZonedDateTime.now(clock.withZone(MST_TIMEZONE));
     return start.isBefore(nowInMST);
 }

 public static void main(String[] args) {
     Clock clock = Clock.systemDefaultZone();
     DateValidator validator = new DateValidator(clock);
      // Testing with a date string
      String dateStr = "2024-06-28 15.00.00";
      boolean isValid = validator.isValidDate(dateStr);
      System.out.println("Is valid date: " + isValid);
  }

Надеюсь, поможет

Ответ принят как подходящий

Я думаю, что проблема у вас здесь в следующем:

LocalDateTime.now(clock).atZone(MST_TIMEZONE)

Это будет делать разные вещи в зависимости от часового пояса JVM, в котором вы его запускаете.

LocalDateTime.now(clock) покажет вам местное время в часовом поясе JVM — скажем так, поскольку мы оба находимся в Лондоне 2024-06-28 16:46:23. Вызов atZone(MST) для этого дает вам ZonedDateTime, то есть 2024-06-28 16:46:23 -08:00.

Если бы вы запустили это на сервере в Фениксе, LocalDateTime.now(clock) получил бы 2024-06-28 08:46:23; вызов atZone(MST) дает вам 2024-06-28 08:46:23 -08:00.

Если вы хотите получить текущее время в MST_TIMEZONE, измените его на:

clock.now().atZone(MST_TIMEZONE)

clock.now() дает вам Instant, который не зависит от часового пояса. Instant, соответствующий времени, которое я написал выше, — это Instant.ofSeconds(1719593183L). Преобразование этого в ZonedDateTime дает LocalDateTime в этой зоне плюс зону.

Вызов LocalDateTime.now сбивает с толку, поскольку этот класс не может представлять момент. И этот звонок ненужен. Существуют более эффективные подходы.

Basil Bourque 28.06.2024 21:32

Я рекомендую более общее решение для приложения SpringBoot.

  1. Создайте компонент Bean, настроенный на основе параметра в вашем application.yaml, который определяет ZonedId. Бин вызывает что-то вроде ZoneId.of(zoneId)
  2. Создайте второй Clock Bean, настроенный на основе этого ZoneId bean-компонента.

Тогда в любом классе Сервиса вам нужны часы, которые вы вводите clock и можете быть уверены, что у них правильная Зона. Затем вы можете использовать его следующим образом: ZonedDateTime.now(clock)

И если вы хотите написать модульный тест, который зависит от часов, вы можете использовать «Фиксированные часы», например. Clock.fixed(Instant.parse("2021-01-01T08:20:50Z"), ZoneOffset.UTC) что дает вам предсказуемые и проверяемые значения часов/зон в ваших тестах.

Вот как это сделать (1) и (2) в Котлине; да, я знаю, что ваш вопрос касается Java, но я уверен, что вы сможете понять эту идею:

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Clock
import java.time.ZoneId

@Configuration
class ClockConfig {
    @Bean
    fun zoneId(@Value("\${zone-id}") zoneId: String): ZoneId {
        return ZoneId.of(zoneId)
    }

    @Bean
    fun clock(zoneId: ZoneId): Clock {
        return Clock.system(zoneId)
    }
}

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

karvai 28.06.2024 19:05

вр; доктор

LocalDateTime                                              // Represent a date with time-of-day but lacking the context of an offset-from-UTC or a time zone. Does *not* represent a moment, is *not* a point on the timeline.
.parse(                                                    
    "2024-06-28 15.00.00" ,                                // Best to *not* invent custom formats for such textual inputs. Better to exchange date-time values textually using only ISO 8601 standard formats. 
    DateTimeFormatter.ofPattern( "uuuu-MM-dd HH.mm.ss" )
)                                                          // Returns a `LocalDateTime` object.
.atZone( ZoneId.of( "America/Phoenix" ) )                  // Returns a `ZonedDateTime` object. Determines a moment, a point on the timeline, by applying the context of a time zone. 
.toInstant()                                               // Returns a `Instant` object. Same point on the timeline, but seen through an offset-from-UTC of zero hours-minutes-seconds.
.isBefore( Instant.now() )                                 // Compares to the current moment as seen in UTC (an offset of zero). 

UTC — твой друг

Программисты и системные администраторы должны научиться думать и работать в формате UTC , моментах даты и времени, которые видны со смещением на ноль часов-минут-секунд от временного меридиана UTC. Считайте UTC единым истинным временем. Все остальные часовые пояса — всего лишь вариации.

Поэтому мы переопределяем вашу проблему как две фазы:

  • Мы получаем текстовый ввод даты с временем суток. К сожалению, у этого ввода есть два недостатка. (A) Текст не в стандартном формате ISO 8601. (B) Текст представляет собой момент, видимый на настенных часах/календаре определенного часового пояса (America/Phoenix), но этот важный факт не включен во входной текст.
  • Мы должны сравнить момент, представленный этим текстовым вводом, с текущим моментом. Мы хотим знать, является ли предполагаемый момент прошлым или будущим.

ИСО 8601

Идеальным решением проблемы ошибочных входных данных было бы информирование издателя этих данных о стандарте ISO 8601. Вместо этого входные данные 2024-06-28 15.00.00 будут разделены 2024-06-28T15:00:00-07:00.

OffsetDateTime

Классы java.time по умолчанию используют форматы ISO 8601. Таким образом, такую ​​стандартную строку можно анализировать напрямую без необходимости определения какого-либо шаблона форматирования. Мы получаем объект OffsetDateTime.

OffsetDateTime odt = OffsetDateTime.parse( "2024-06-28T15:00:00-07:00" ) ;

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

LocalDateTime

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

DateTimeFormatter f = DateTimeFormatter.ofPattern( "uuuu-MM-dd HH.mm.ss" ) ;
LocalDateTime ldt = LocalDateTime.parse( "2024-06-28 15.00.00" , f ) ;

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

ZonedDateTime

Нас заверили, что наш текстовый ввод даты и времени предназначен для просмотра в часовом поясе America/Phoenix. Итак, назначьте эту зону, чтобы создать объект ZonedDateTime.

ZoneId z = ZoneId.of( "America/Phoenix" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ;

Instant

Теперь у нас есть момент, точка на временной шкале. Перейдите из этой зоны в UTC, наше Единое Истинное Время.

Самый простой способ настроить UTC — извлечь объект Instant. Класс Instant является строительным блоком инфраструктуры java.time. Он представляет собой момент, видимый в формате UTC, всегда в формате UTC.

Instant instant = zdt.toInstant() ;

Запечатлейте текущий момент.

Instant now = Instant.now() ; 

Сравнивать.

Boolean isPast = instant.isBefore( now ) ;

Сосредоточьтесь на UTC

проверьте, не предшествует ли эта дата текущему времени MST.

Нет. Измените свое мышление и сосредоточьтесь на UTC.

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

Псевдочасовые пояса

МСТ

«MST» не является реальным часовым поясом. Этот текст представляет собой псевдозону, которая лишь намекает на реальную зону и указывает, соблюдается ли летнее время (DST) в эту дату и время. Такие псевдозоны не стандартизированы; они даже не уникальны!

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

Реальные часовые пояса имеют названия в формате Continent/Region.

Никогда не звоните LocalDateTime.now

LocalDateTime.now(

Я не могу представить сценарий, в котором звонок LocalDateTime.now будет правильным или оптимальным решением. «Сейчас» означает, что вы намереваетесь достичь определенной точки на временной шкале. Класс LocalDateTime не может представлять конкретную точку на временной шкале.

Установите серверы в UTC

По сути, если я окажусь на другом сервере в Австралии.

На серверах часовой пояс по умолчанию должен быть установлен в формате UTC.

Также имейте в виду, что на сервере может быть несколько часовых поясов по умолчанию. К ним относятся значения по умолчанию в ОС, значения по умолчанию на любом сервере базы данных и значения по умолчанию в JVM.

Вы всегда должны писать свой код таким образом, чтобы избежать зависимости от внешних факторов, таких как текущий часовой пояс хостовой ОС по умолчанию. Вы не хотите, чтобы ваш код сломался только из-за того, что какой-то системный администратор изменил настройку часового пояса. Ключевым моментом является всегда явное указание желаемого/ожидаемого часового пояса; всегда передавайте необязательный аргумент часового пояса или смещения, а не полагайтесь неявно на значение по умолчанию.

Обратите внимание, что пример кода в этом ответе невосприимчив к изменениям в зоне по умолчанию ОС.

Ввод 2024-06-28 15.00.00 будет общим вместо 2024-06-28T15:00:00-07:00. Еще лучше 2024-06-28T15:00:00-07:00[America/Phoenix].

g00se 28.06.2024 19:42

@ g00se Добавление названия часового пояса разумно, но нестандартно. ISO 8601 касается только смещений, а не часовых поясов. Здесь также нет необходимости, поскольку зона полезна только при выполнении математических операций с датой и временем, прибавлении/вычитании количества времени.

Basil Bourque 28.06.2024 21:30

Другие вопросы по теме