【问题标题】:Creating a promotion that starts at 9am for all stores worldwide为全球所有商店创建从上午 9 点开始的促销活动
【发布时间】:2019-04-24 03:04:33
【问题描述】:

例如,假设全球所有商店的商店促销活动从上午 9 点开始。这意味着,芝加哥的商店从美国中部标准时间上午 9 点开始,西雅图的商店从太平洋标准时间上午 9 点开始,英国的商店从格林威治标准时间上午 9 点开始。

在 Postgres 上的 promotions 表中,我们会将此次促销活动的开始时间设置为 09:00:00。

每家商店都有一台带有网络浏览器的计算机,用于查找可用的促销活动。它需要将其本地时间传递给服务器,以便服务器可以返回该本地时间的所有促销活动。因此,我们需要找到一种方法来捕获 JavaScript 中的本地时间,对其进行编码,将其发送到 Java 后端,对其进行重构,然后将其与promotions 表中的开始时间进行比较。

当然,当地时间取决于时区。如果芝加哥是上午 9 点,那么芝加哥的商店应该告诉服务器现在是上午 9 点。如果不指明时区就发送 UTC 时间是徒劳的。

问题:什么是在 JavaScript 中捕获本地时间(基于时区)的好方法,对其进行编码,将其发送到 Java 后端,将其重构为 Java Date,以及然后将 Java Date 与 Postgres 数据库中上午 9 点的促销开始时间进行比较?

我的(不满意)方法:我能想到的最好的方法是使用 JavaScript 的 Date.getTime 方法发送 UTC 时间(以毫秒为单位),以及时区偏移量,可以以分钟为单位计算使用 JavaScript 的 Date.getTimezoneOffset 方法并转换为毫秒。从 UTC 时间中减去时区偏移量(以毫秒为单位),然后我们可以从结果差中创建一个 Java Date 对象。如果是芝加哥的上午 9 点,那么希望 Java Date 将存储上午 9 点。然而,这种方法有点奇怪的是,Java Date 实际上将存储 UTC 上午 9 点,即使它代表 CST 上午 9 点。这只是我对这种方法不满意的原因之一。你能想出更好的办法吗?

【问题讨论】:

  • 你为什么使用j.u.Date?它已经被弃用了 4 年。你想要LocalDateTime
  • @Michael 因为我们的软件至少有五年历史了 :) 我知道...java.time 是新标准...但不幸的是我们没有在这部分代码中使用它。
  • 这不是借口。使用 ThreeTen-Backport 或 Joda Time,或升级到 Java 8。
  • @ktm5124 因为 LocalDateTime 表示没有区域的日期时间,这正是您想要的。 j.u.Date 划为 UTC。

标签: javascript java date time timezone


【解决方案1】:

Postgres

在 Postgres 中,当您指的是某个日期的任何地方的上午 9 点或任何地方的上午 9 点时,请使用列数据类型 TIMESTAMP WITHOUT TIME ZONE。输入中包含的任何时区或与 UTC 的偏移量都将被忽略,日期和时间按原样(无调整)并存储。这种数据类型故意缺少time zoneoffset-from-UTC 的任何概念。

对于没有日期的时间,使用TIME WITHOUT TIME ZONE 数据类型。 Postgres 还提供TIME WITH TIME ZONE 只是因为它是 SQL 规范所要求的;这个WITH 类型是无意义的,永远不应该使用。

Postgres 是此类项目的绝佳选择,因为它在 its data typesits functions 中都提供了出色的日期时间支持。数据库的日期时间特征差异很大。

Java

在 Java 后端,只使用现代的 java.time 类。这些年前,它取代了与最早的 Java 版本捆绑在一起的 可怕 旧日期时间类。

如果尚未使用 Java 8 或更高版本,请在 ThreeTen-Backport 项目的 Java 6 和 7 的反向移植中找到几乎所有相同的功能。非常值得将这个库添加到您的项目中。来自为您带来 java.time 类和 Joda-Time 项目的同一批人,均由同一个人 Stephen Colebourne 领导。

LocalDateTime

java.time 中,使用 LocalDateTime 类表示您的意思是某个日期的任何地方/任何地方的上午 9 点。与 Postgres 中的 TIMESTAMP WITHOUT TIME ZONE 一样,此类故意缺少任何区域或偏移量的概念。

LocalDateTime ldt = LocalDateTime.of( 2018 , 1 , 23 , 15 , 0 , 0 , 0 ) ;  // 3 PM on 23rd of January this year.

LocalTime

如果您只指时间,没有日期,请使用 LocalTime 类。

LocalTime lt = LocalTime.of( 15 , 0 ) ;  // 3 PM.

JDBC

从 JDBC 4.2 及更高版本开始,您可以通过 getObjectsetObject 方法与数据库交换 java.time 对象。

LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ) ;

如果您的 JDBC 驱动程序尚未更新到 4.2,则回退到糟糕的旧遗留类,但立即转换为 java.time 类。

鉴于遗留类缺少一个没有时区的日期加时间类,我们必须伪造它。使用java.sql.Timestamp,它代表 UTC 中的时刻,分辨率为纳秒,忽略它是 UTC 的事实。

java.sql.Timestamp ts = myResultSet.getTimestamp( … ) ;

对于 Java 8 及更高版本,使用添加到旧类的新方法进行转换。首先转换为java.time.Instant,它也代表UTC 中的一个时刻,分辨率为纳秒。然后通过有效地去除 UTC 的概念转换为LocalDateTime

Instant instant = ts.toInstant() ;  // Convert from legacy class to modern one.
LocalDateTime ldt = LocalDateTime.ofInstant( instant , ZoneOffset.UTC ) ;  // Remove the concept of UTC (or any other offset or zone) from our data.

对于使用 ThreeTen-Backport 库的 Java 6 和 7,请使用其实用程序 DateTimeUtils 类中的转换方法。

org.threeten.bp.Instant instant = org.threeten.bp.DateTimeUtils.toInstant( ts ) ;  // Convert from legacy class to modern.
org.threeten.bp.LocalDateTime ldt = LocalDateTime.ofInstant( instant , ZoneOffset.UTC ) ;  // Remove the concept of UTC (or any other offset or zone) from our data.

ZonedDateTime

根据定义,Local… 类没有真正的意义,除非您将它们放在时区的上下文中。 LocalDateTime 不是片刻,不是代表时间线上的一个点吗。

continent/region 的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用 2-4 个字母的缩写,例如 PSTBSTESTIST,因为它们不是真正的时区,没有标准化,甚至不是唯一的(!) .

LocalDateTime ldt = 
    LocalDateTime.of(
        LocalDate.of( 2018 , Month.January , 23 ) ,
        LocalTime.of( 9 , 0 ) 
    )
;

ZoneId zLosAngeles = ZoneId.of( "America/Los_Angeles" ) ;  // Seattle time zone.
ZonedDateTime zdtSeattle = ldt.atZone( zLosAngeles ) ;

ZoneId zChicago = ZoneId.of( "America/Chicago" ) ;  
ZonedDateTime zdtChicago = ldt.atZone( zChicago ) ;

ZoneId zLondon = ZoneId.of( "Europe/London" ) ;  
ZonedDateTime zdtLondon = ldt.atZone( zLondon ) ;

我们有三个ZonedDateTime 对象:zdtSeattlezdtChicagozdtLondon。这些都是今年早些时候 1 月 23 日上午 9 点。了解这是三个非常不同的时刻,每一个都比你向东早几个小时。它们都有相同的挂钟时间(23 日上午 9 点),但时间轴上的三个不同点。

JavaScript

虽然我对 JavaScript 的了解还不足以肯定地说,但我怀疑那里有任何库可以像日期时间处理那样丰富。 java.time 框架是业界领先的。

至于 Web 客户端用户界面开发,我使用 Vaadin,所以这不是问题:后端的纯 Java 会自动生成 Web 浏览器所需的 HTML/CSS/DOM/JavaScript。

找到一种在 JavaScript 中捕获本地时间的方法

关于在客户端机器中检测当前默认时区,我不是专家,但我记得浏览器不返回指定时区,只返回与 UTC 的偏移量。有关可能的解决方案,请参阅Answer by Matt Johnson。在任何应用程序(桌面或网络)中,最终,如果正确的时区至关重要,那么您必须询问或与用户确认所需/预期的时区。明智的做法是始终在用户界面的某个位置指出您的应用正在使用哪个时区。

如果您需要在 Java 后端和前端的 JavaScript 代码之间交换日期时间值,您主要有两种选择:

  • ISO 8601
  • 从纪元开始计数

ISO 8601

ISO 8601 标准定义了各种用于交换日期时间值的文本格式。这些都是为了避免歧义而明智地设计的。它们很容易被机器解析,并且很容易被跨文化的人类阅读。

java.time 类在生成/解析字符串时默认使用这些格式。

从纪元开始计数

我不推荐这种方法,因为它令人困惑且容易出错,并且在发送或接收的人和图书馆之间容易产生歧义和不正确的假设。

epoch reference date 是用作基线的时间点。然后一些向前或向后的计数是由一些粒度组成的。

一个大问题是各种系统都使用dozens of epoch referencesjava.time 类默认使用 UTC 1970 年第一刻的 Unix time 纪元,1970-01-01T00:00Z。

另一个问题是有很多粒度,例如整秒、毫秒、微秒和纳秒。程序员必须清楚地记录/传达正在发挥作用的粒度。

如果您要将您的三个商店的三个开业时刻发送到 JavaScript 作为从 epoch 计数,您将发送三个不同的数字。

long millisecondsSeattle = zdtSeattle.toInstant().toEpochMilli() ;
long millisecondsChicago = zdtChicago.toInstant().toEpochMilli() ;
long millisecondsLondon = zdtLondon.toInstant().toEpochMilli() ;

在三个不同的时刻产生三个不同的数字。


关于java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

您可以直接与您的数据库交换 java.time 对象。使用符合JDBC 4.2 或更高版本的JDBC driver。不需要字符串,不需要java.sql.* 类。

从哪里获得 java.time 类?

ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

【讨论】:

  • 同意你的所有观点(和往常一样),除了建议 Vaadin 解决 JS 方面。这掩盖了问题中的一个关键点(恕我直言)。请参阅我对那部分的回答,并随时复制以供您参考和将来的帖子。谢谢。
  • @MattJohnson 感谢您的反馈。我添加了一些我对 JavaScript 知之甚少的内容,并添加了指向您的答案的链接。
  • 这是一个很好的答案。非常感谢!
【解决方案2】:

您不需要获取用户的本地时间,只需获取他们的 IANA 时区标识符,例如 "America/Los_Angeles"。然后可以在接受时区的 API 中的 Java 后端代码中使用它。

在大多数现代浏览器中,您可以像这样捕获时区 ID:

Intl.DateTimeFormat().resolvedOptions().timeZone

如果您需要支持较旧的浏览器,有几个库将在可用时使用此 Intl API,但在不可用时将退回到有根据的猜测。 More on this here.

【讨论】:

  • 由于我们一直使用 java.util.Date,实际上可能很难从 IANA 时区标识符中解析日期。你会碰巧知道不同吗?我希望我们可以转换为 java.time,但这是一段仍然使用 java.util.Date 的旧代码。
  • 您不会将其解析为Date,而是使用java.util.TimeZone,可能与java.util.Calendar 和/或SimpleDateFormat 结合使用。 IANA 时区 ID 不是新的 java.time API 所独有的(尽管如此,您应该尽可能选择那些)。
猜你喜欢
  • 2015-02-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多