【问题标题】:Java ZonedDateTime.toInstant() behaviorJava ZonedDateTime.toInstant() 行为
【发布时间】:2019-05-09 19:15:33
【问题描述】:

我在 2018 年 12 月 7 日运行以下表达式。

我看到了这样的差异:

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 导致返回 November 7th,就像在 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 规则)试图将其简化为易于理解的形式,这可能破坏了大部分必要的上下文,使其变得模糊。让我再试一次。

正如我在 cmets 中解释的那样,我正在将现有的 Python 代码(更积极地维护并且哪个客户端希望保持完整)转换为现有的 Java 代码(没有正确维护并偏离 Python 的遗留代码)一段时间后的逻辑)。两个代码库都需要在功能上彼此相提并论。 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)

它基本上采用配置的时区,在该时区中获取当前时刻,从中减去指定的天数,将其转换为该日期的开始,从而在查询时接收要使用的范围的下限DB,类似于开始时间: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() 的调用似乎是罪魁祸首,我想更深入地了解其背后的原因。

In his answer, @ernest_k 提出了一个似乎有效的解决方案,但我仍然不太明白,从 cmets 中的问题到他的回答中可以清楚地看出这一点。

我根据@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(),这似乎是消除Dates 的合适人选,同时替换上面的旧toBeginningOfDay(Date d) 方法.不确定它是否在做同样的事情 - 我还没有尝试过这个想法,所以非常欢迎提出建议。

因此,由于上述所有磨难,我的问题是围绕toInstant() 行为开始的,当它传递一个区域 id 时,它是在该区域中瞬间转换为 TO 还是 来自它,还是什么

我想对于我所描述的情况,我们只关心 DB 查询中的下限时间是通过将当前时间的一些 一致 标记(其上限)与其原来的时间进行比较而形成的在过去 N 天内在同一个地方(时区?),因此将其与 UTC 进行比较应该达到目的。

这样就不需要通过该区域了吗?

现在,似乎已经找到了一个解决方案,问题围绕着上述方法的合理性和偶然发现的解决方案 - 它是最佳方案,还是围绕 Java 计时库的最佳实践等。代码需要适用于最终部署代码库的任何时区,这就是该区域通过配置传入的原因。

另外,我想知道当/如果数据库本身从代码库的其余部分部署到外部部署并配置为在其他时区持久保存数据时,情况是否会发生变化。但这可能是另一个问题。

【问题讨论】:

  • 您只需要 11 月 7 日,还是还需要与现在相同的时间?
  • 逻辑是从当天减去某些(可配置的)天数。区域 id ('America/New_York') 也是参数化和配置的,为了简单起见,我只是在这里对这些值进行硬编码。因此,如果在 12 月 8 日运行,我预计 11 月 8 日将返回,12 月 7 日至 11 月 7 日将返回,依此类推。
  • 例如,如果在Fri, Dec-7, 8:14pm 上运行,Python 会返回:datetime: 2018-11-07 20:13:55.063888-05:00,但 Java 不会。我希望在 Java 方面也能得到类似的结果。
  • 对于我们这些不了解(足够)Python 的人,也许如果你解释一下你需要什么日期,我们可以更好地建议。
  • 我已经用我所追求的伪代码更新了这个问题。这是对现有代码库的重构,以使其与 Python 相提并论,因此我试图以最少的更改来做到这一点,因此删除它们对 Date 的(大量)使用是不可取的(至少在这一点上) .

标签: java python django java-time pytz


【解决方案1】:

tl;博士

ZonedDateTime.toInstant() 将时刻从时区调整为 UTC。您最终会得到相同的时刻、不同的挂钟时间以及可能不同的日期,用于时间轴上的同一同时点。您所看到的不是问题,不是差异

您的问题不在于减去 30 天。真正的问题:

  • 不了解时区会影响日期
  • 将日期与天数混为一谈

此外,您的问题含糊不清。说“30 天前”至少可以表示三种不同的含义:

  • 30 * 24 小时
  • 范围从纽约时区三十天前的 22:44 到纽约时间现在的 22:44
  • 今天在纽约看到的一整天,以及在纽约看到的日历上倒退 30 天的整个天。

下面介绍了所有三种可能性,带有示例代码,标有

⑦? ??? ↔ ??? ⑧?

12 月 7 日,午夜前不久(22:44),爱丽丝在她的纽约公寓里决定给她在冰岛雷克雅未克的朋友鲍勃打电话。 Bob 不敢相信他的电话响了,他看了看床头柜上的时钟,发现时间几乎是凌晨 4 点(03:44)。鲍勃的精美数字时钟显示日期为 12 月 8 日,而不是 7 日。同一时刻,时间轴上的同一点,不同的挂钟时间,不同的日期

冰岛人民全年都使用 UTC 作为他们的时区。 2018 年 12 月,纽约比 UTC 晚 5 小时,因此比冰岛晚 5 小时。在纽约,“昨天”是第 7 次,而在冰岛,“明天”是第 8 次。不同的日期,相同的时刻。

所以忘了减去三十天吧。每当您在接近午夜的纽约停留片刻,然后调整到 UTC 时,您就会将日期向前移动。

没有差异,没有额外的一天。对于任何给定的时刻,日期在全球各地的时区都会有所不同。在大约 26-27 小时的时区范围内,它总是在某个地方“明天”和“昨天”。

Another Answer 建议让LocalDateTime 参与这个问题。这是不明智的。该类故意缺少任何时区或与 UTC 偏移的概念。这意味着LocalDateTime 不能 代表一个时刻。 LocalDateTime 代表上述 26-27 小时范围内的潜在时刻。在这里涉及该类是没有意义的。

取而代之的是,使用OffsetDateTime 与使用时区的 [ZonedDateTime][2] 相比,使用与 UTC 的偏移量查看片刻。

偏移量和区域之间有什么区别?偏移量只是几个小时-分钟-秒,不多也不少。相比之下,区域更多。区域是特定区域的人们使用的偏移量的过去、现在和未来变化的历史。因此,时区总是比单纯的偏移更可取,因为它带来了更多信息。如果你特别想要 UTC,你只需要一个偏移量,一个零时分秒的偏移量。

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

这里看到的zdtodt 都代表同一时刻,时间轴上的同一点,不同的挂钟时间,就像上面的 Alice 和 Bob 示例。

天!= 日期

如果要查询三十天前的范围,则必须定义“天”的含义。

➥ 你的意思是 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. 

可能能够通过您的 JDBC 驱动程序与您的数据库交换Instant。但Instant 是可选的,而JDBC 4.2 及更高版本需要支持OffsetDateTime。如果是这样,让我们​​重新编写代码。

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 可能类似于以下内容。

注意我们使用半开放方法来定义时间跨度,其中开始是包含,而结束是排除。这通常是最佳实践,因为它避免了找到无限可分的最后时刻的问题,并且它提供了整齐的邻接跨度而没有间隙。所以我们使用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 ) ;

【讨论】:

  • 也许不清楚:我减去 30(或 N)天(从现在开始)以形成用于数据库查询的日期范围(如果没有明确的开始和结束日期提供了该查询),而不是因为我只是想将日期更改为不同的值。这就是我要转换的东西的 Python 端的应用程序逻辑。
  • @SimeonLeyzerzon 你的困惑与减去天数无关。问题在于不了解时区处理,以及将日期与天数混为一谈。
  • @SimeonLeyzerzon 您的问题含糊不清。说“30 天前”可以表示至少三种不同的东西:(a) 30 * 24 小时,(b) 从纽约时区 30 天前的 22:44 到现在纽约时间的 22:44 的范围, (c) 在纽约看到的今天的一整天,以及在纽约看到的日历上倒退 30 天的整个天。我用示例代码在我的答案中涵盖了所有三个。任君挑选。
  • 该时刻用于在数据库查询中形成下限,因此它会尝试计算提供的天数以获取相应过去一天的同一时刻,然后从开始发现的那一天。我在原始查询中添加了更多上下文,希望现在上下文更清晰。
【解决方案2】:

那个“额外的一天”并不是真正的额外的一天。纽约的2018-11-07T22:44:11 相当于UTC 的2018-11-08T03:58:01(它是同一时间点)。区别只是5 小时,而不是一天(当我用谷歌搜索时,我看到纽约是GMT-5)。

ZonedDateTime#toInstant 返回一个代表同一时间点的Instant 实例(以UTC 表示):

将此日期时间转换为 Instant。 这将返回一个 Instant,表示时间线上与此日期时间相同的点。该计算结合了本地日期时间和偏移量。

如果您想在转换为即时时使用偏移量,那么您或许应该使用LocalDateTime

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

这告诉它转换为好像它已经是 UTC 时间(但此处适当的警告:这会更改日期/时间值

【讨论】:

  • 该建议似乎有效,但我仍然不明白为什么。需要转换为LocalDateTime。那和带有指定区域ID的ZonedDateTime有什么区别?
  • @SimeonLeyzerzon ZonedDateTime.toInstant() 考虑了(相关时区的)偏移,但LocalDateTime.toInstant(ZoneOffset.UTC) 没有(LocalDateTime 没有时区或偏移信息,并指定@987654334 @ 强制它将时间视为已经是 UTC 时间,因此它不会添加/删除任何偏移值)。但您需要确保这是您想要的,因为它有效地改变了“时间点”值。
  • 这两个表达式是否等效:LocalDateTime.now().minusDays(days)ZonedDateTime.now(ZoneId.of("America/New_York")).minusDays(days).toLocalDateTime()?你说:it effectively changes the "point in time" value 是什么意思?
  • 我相信它们会,但前提是它们在纽约的计算机上运行。 (以便LocalDateTime.now() 获取所需的值)
【解决方案3】:

首先,尽可能避免使用老式的Datejava.time,现代 Java 日期和时间 API,为您提供所需的所有功能。

有时我们确实需要 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_York。

DateInstant 大致相同,但打印不同。这意味着他们的 toString 方法的行为不同,这可能会令人困惑。每个都是一个时间点,它们都不是日期(尽管其中一个的名称)。所有时区的日期都不会相同。

Date.toString 获取 JVM 的时区设置并使用它来生成它返回的字符串。另一方面,Instant.toString 始终为此使用 UTC。这就是为什么在同一时间点打印不同的日期和时间的原因。幸运的是,它们都打印了一些时区信息,因此至少可以看到差异。 Date 打印出 EST,虽然不明确,但在这种情况下表示东部标准时间。 Instant 打印 Z 与 UTC 或“祖鲁时间”的偏移量为零。

【讨论】:

  • 澄清一下,我正在寻找上述 Python 表达式的功能等效翻译。在 Java 方面,只要我将 toInstance() 添加到 (run date 减去 days offset) 表达式,我就会再次回到上升的时间值(在我的情况下从第 7 到第 8),这是不可取。我确实有一些代码,然后将 11 月 7 日的日期时间转换为当天的开始。为了清楚起见,它被排除在外。我不确定是什么导致了这种行为以及天气是否与时区有关。希望这是有道理的。
  • 很抱歉我不知道 Django/Python 类型。我所知道的是,Java Date 无法为您提供一天中的日期和时间——只能提供与任何时区或 UTC 偏移量无关的时间点。
猜你喜欢
  • 1970-01-01
  • 2020-11-02
  • 2023-04-09
  • 1970-01-01
  • 2017-03-21
  • 2011-07-16
  • 2018-08-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多