java - Java ZonedDateTime.toInstant()行为

标签 java python django java-time pytz

我将于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导致返回11月7日,就像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规则)试图将其简化为易于消化的形式,这很可能破坏了大多数必要的上下文,使其变得模糊。让我再尝试一次。

正如我在评论中所解释的那样,我正在将现有的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)


它基本上采用配置的时区,在该时区中获取当前时刻,从中减去指定的天数,将其转换为该日期的开始,从而获得查询数据库时要使用的范围的下限如开始时间: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提出了一个似乎可行的解决方案,但我仍然不太明白,从他对他的回应的评论中可以清楚地看出来。

我基于@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(),似乎是在替换上面的旧版Date方法时从图片中消除toBeginningOfDay(Date d)的合适人选。不确定它是否在做同样的事情-我尚未尝试过该想法,因此,建议非常受欢迎。

因此,对于上述所有麻烦,我的问题都是围绕toInstant()行为开始的,当它传递区域ID时,它是转换为该区域中的即时数据还是从该区域转换为即时数据?

我猜对于我正在描述的情况,我们只关心数据库查询中的下限时间是通过将当前时间的某些一致标记(其上限)与相同位置(时区?)中的时间进行比较而形成的。过去N天,因此将其与UTC进行比较应该可以达到目的。

这样是否会使不必要地通过区域?

现在,似乎已经找到了一种解决方案,问题围绕着上述方法的合理性和偶然发现的解决方案-它是围绕Java时序库的最佳方案,最佳实践等。代码需要可以在最终部署代码库的任何时区工作,这就是为什么通过配置传递时区的原因。

另外,我想知道是否//如果将数据库本身部署在非代码库的其他地方,并且配置为将数据保留在其他时区中,情况是否会发生变化。但这可能是另一个问题。

最佳答案

tl; dr

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

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


不了解时区会影响日期
将日期与天合计


此外,您的问题不明确。说“ 30天前”可能意味着至少三件事:


30 * 24小时
范围从三十个日历日前在纽约时区的22:44到现在的纽约时间
如纽约所示,今天一整天,如日历所示,可以追溯到日历的30天。


下面用标有的示例代码介绍了这三种可能性。

⑦🕥🇺🇸📞↔📞🇮🇸⑧🕞

12月7日,午夜(22:44)之前,爱丽丝在纽约的公寓里决定给冰岛雷克雅未克的朋友鲍勃打电话。 Bob简直不敢相信自己的电话在响,看着床头柜上的时钟,时间已经快凌晨4点(03:44)。鲍勃(Bob)花哨的数字时钟将日期显示为12月8日,而不是7月。相同的同时时刻,时间轴上的相同点,不同的时钟时间,不同的日期。

冰岛人民全年都使用UTC作为时区。纽约在2018年12月比UTC落后五个小时,因此比冰岛落后五个小时。在纽约,“昨天”是第七名,在冰岛,是“明天”第八名。不同的日期,同一刻。

因此,忘记减去三十天。每当您在接近午夜的纽约时间,然后调整为UTC时,您都将向前移动日期。

没有差异,没有增加额外的一天。在任何给定时刻,日期都会随时区在全球范围内变化。时区的范围大约为26-27小时,因此总是在“明天”和“昨天”某个地方。

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

取而代之的是,将OffsetDateTime用于从UTC偏移量观看的时刻,而[ZonedDateTime][2]使用时区。

偏移量和区域之间有什么区别?偏移量仅是数小时-分-秒,仅此而已。相比之下,区域更多。区域是特定区域的人们使用的偏移的过去,现在和将来的历史记录。因此,时区总是比单纯的偏移更可取,因为它带来了更多的信息。如果要专门使用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 ) ;

关于java - Java ZonedDateTime.toInstant()行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53679397/

相关文章:

Java:它创建的定时器和线程

java - 将 api (rito api) 与 java (android) 一起使用

Python/OpenCV - 不检测网格

python - 递归 pyinotify 监视阻止子目录被删除

java - 斯坦福 NLP 语法关系类型?

python - 如何设置 Google Cloud Function HTTP 响应的内容类型

python - 为什么我无法在 Django orm 中从数据库中检索日期时间值?

python - 为 Django 设置 SQLite3 时出现错误配置的数据库错误

python - "Symbol not found: __cg_jpeg_resync_to_restart"与 djangostack

java - 从 servlet 和 jsp 打印到 txt 文件