我希望传递一个日期字符串并检查该日期是否早于当前 MST 时间。

我希望能够在本地和部署时可靠地测试它。

我本地位于英国。当我部署它时,服务器位于凤凰城。

英国现在的时间是2024-06-28 15.45.00,当我传入时,此逻辑当前对 isValidDate 产生 true 2024-06-28 15.00.00

但是我将区域设置为 MST。我以为这是错误的。MST

时间现在是早上 8 点。所以不是之前。它似乎继续与英国时间相反。

我该如何更新它,以便在部署时,它会根据 MST 时间检查日期字符串。并在本地继续运行 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));
}

3

  • 2
    我可以对您的命名提出异议吗:您parseDate返回了一个ZonedDateTime;您isValidDate接受了一个称为的值*DateTime。您应该选择能够准确描述您正在做的事情的名称,尤其是在日期和时间这种容易混淆的空间中。


    – 

  • 当我们谈论命名时…凤凰城现在夏季的时区是 MDT,而不是 MST。


    – 


  • 1
    处理日期的一般规则:在代码和数据库(如果有)内部将所有内容都存储在 UTC 中。在输入时将本地转换为 UTC,在输出时将 UTC 转换为本地。类java.time.Instant是你的朋友。


    – 



最佳答案
4

我认为你在这里遇到的问题是这样的:

LocalDateTime.now(clock).atZone(MST_TIMEZONE)

根据运行它的 JVM 的时区,这将执行不同的操作。

LocalDateTime.now(clock)将为您提供 JVM 时区中的本地时间 – 因为我们都在伦敦,所以我们假设2024-06-28 16:46:23。调用atZone(MST)会为您提供 ,ZonedDateTime2024-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该时区的 ,加上时区。

1

  • 1
    调用LocalDateTime.now很令人困惑,因为该类不能表示时刻。而且这种调用是不必要的。有更好的方法可用。


    – 

总结

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 时间子午线之间的

因此我们将您的问题重新定义为两个阶段:

  • 我们收到一个包含日期和时间的文本输入。不幸的是,这个输入有两个缺陷。(A)文本不是标准格式。(B)文本表示特定时区()的挂钟/日历中显示的时刻,America/Phoenix但这个关键事实并未包含在输入文本中。
  • 我们必须将此文本输入所代表的时刻与当前时刻进行比较。我们想知道预期的时刻是过去还是将来。

ISO 8601

解决输入错误的理想方法是教育数据发布者了解 标准。共享的输入2024-06-28 15.00.00则不是2024-06-28T15:00:00-07:00

OffsetDateTime

java.time类默认使用 ISO 8601 格式。因此,这样的标准字符串可以直接解析而无需定义任何格式模式。我们得到一个对象。

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

使用 ISO 8601 标准格式来更正已发布的数据确实是理想的解决方案。ISO 8601 的发明专门用于以文本形式交换日期时间值,这种方式既适合机器,也适合跨文化的人类。

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。因此,指定该时区以创建对象。

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

Instant

现在我们有一个时刻,时间线上的一个点。从该时区调整为 UTC,即我们的唯一真实时间。

调整为 UTC 的最简单方法是提取一个Instant对象。该类是java.timeInstant框架的构建块。它表示以 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 中的默认时区。

您应该始终以这样的方式编写代码,避免依赖外部因素,例如主机操作系统的当前默认时区。您不希望您的代码仅因为某些系统管理员更改时区设置而中断。关键是始终明确指定所需/预期时区;始终传递可选时区或偏移参数,而不是隐式依赖默认值。

请注意,此答案中的示例代码如何不受操作系统默认区域更改的影响。

2

  • 输入 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 仅处理偏移量,而不处理时区。这里也没有必要,因为时区仅在进行日期时间数学运算、增加/减去时间量时才有用。


    – 

您需要确保所有与时间相关的操作都使用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);
  }

希望能帮助到你

我推荐一个更通用的 SpringBoot 应用程序解决方案

  1. application.yaml创建一个 Bean,该 Bean 是根据中定义的参数进行配置的ZonedId。该 Bean 调用类似ZoneId.of(zoneId)
  2. 创建从该beanClock配置的第二个 BeanZoneId

然后,在任何服务类中,您都需要注入一个时钟,clock并确保它具有正确的区域。然后您可以像这样使用它:ZonedDateTime.now(clock)

当您想要编写一个依赖于时钟的单元测试时,您可以使用“固定时钟”,例如,Clock.fixed(Instant.parse("2021-01-01T08:20:50Z"), ZoneOffset.UTC)它为您的测试提供可预测和可测试的时钟/区域值。

以下是如何在 Kotlin 中实现 (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)
    }
}

1

  • 这可能有效,但相信 OP 正在寻找 Java。另外,有一条评论提到已经有一个指向系统默认值的时钟 bean,无法更改。如果采用这种方法,可能需要将其设为主要。


    –