Я хочу передать строку даты и проверить, предшествует ли эта дата текущему времени 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));
}
И пока мы говорим об именовании... часовой пояс в Финиксе сейчас летом - MDT, а не MST.
Общее правило работы с датами без безумия: внутри вашего кода и базы данных (если она есть) храните все в формате UTC. Преобразуйте из местного значения в формат UTC на входе и из формата UTC в локальный на выходе. Класс java.time.Instant
— ваш друг.
Вам необходимо убедиться, что все операции, связанные со временем, выполняются с использованием часового пояса 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
сбивает с толку, поскольку этот класс не может представлять момент. И этот звонок ненужен. Существуют более эффективные подходы.
Я рекомендую более общее решение для приложения SpringBoot.
application.yaml
, который определяет ZonedId
. Бин вызывает что-то вроде ZoneId.of(zoneId)
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. Кроме того, есть комментарий, в котором упоминается, что уже существует тактовый компонент, указывающий на системные настройки по умолчанию, которые нельзя изменить. Потенциально необходимо сделать это основным, если вы собираетесь использовать этот подход.
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 единым истинным временем. Все остальные часовые пояса — всего лишь вариации.
Поэтому мы переопределяем вашу проблему как две фазы:
America/Phoenix
), но этот важный факт не включен во входной текст.Идеальным решением проблемы ошибочных входных данных было бы информирование издателя этих данных о стандарте 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 ) ;
проверьте, не предшествует ли эта дата текущему времени MST.
Нет. Измените свое мышление и сосредоточьтесь на UTC.
Итак, нам нужно проверить, находится ли пройденный момент перед текущим моментом.
МСТ
«MST» не является реальным часовым поясом. Этот текст представляет собой псевдозону, которая лишь намекает на реальную зону и указывает, соблюдается ли летнее время (DST) в эту дату и время. Такие псевдозоны не стандартизированы; они даже не уникальны!
В своем обсуждении и своей логике используйте только часовые пояса реального времени. Псевдозоны следует использовать только в сгенерированном локализованном тексте для отображения пользователю.
Реальные часовые пояса имеют названия в формате Continent/Region
.
LocalDateTime.now
LocalDateTime.now(
Я не могу представить сценарий, в котором звонок LocalDateTime.now
будет правильным или оптимальным решением. «Сейчас» означает, что вы намереваетесь достичь определенной точки на временной шкале. Класс LocalDateTime
не может представлять конкретную точку на временной шкале.
По сути, если я окажусь на другом сервере в Австралии.
На серверах часовой пояс по умолчанию должен быть установлен в формате 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 Добавление названия часового пояса разумно, но нестандартно. ISO 8601 касается только смещений, а не часовых поясов. Здесь также нет необходимости, поскольку зона полезна только при выполнении математических операций с датой и временем, прибавлении/вычитании количества времени.
Могу ли я возразить против вашего именования: у вас есть
parseDate
, возвращающийZonedDateTime
; у вас естьisValidDate
принятие значения под названием*DateTime
. Вам следует выбирать имена, которые точно описывают то, что вы делаете, особенно в такой путанице, как даты и время.