如何使用Java 8 DateTime API从序列值本地日期时间获取POSIX时间(UTC)

我有一个类似于POSIX Time的时间戳,唯一的例外是它不是以UTC计算的。

相反,它是自1970年1月1日午夜以来在特定当地时区内经过的毫秒数。 为了将此值变为Instant ,我必须首先知道其对UTC / GMT的偏移(以毫秒为单位)。

所以问题是:知道本地时区id,例如。 “美国/芝加哥”和自本地大纪元以来的毫秒数,我如何制作一个瞬间(必须用POSIX纪元以来的毫秒构建)?

似乎任何java.time API构造函数都不接受本地Epoch中的毫秒参数。

我有一个解决方案,我首先将本地毫秒日期时间转换为本地格里高利历日期时间(然后我可以从中构建一个LocalDateTime并获得UTC的偏移量),但这看起来好像很多看起来应该很简单。

计算修改时期的瞬间:

 ZoneId tz = ZoneId.of("America/Chicago"); Instant modifiedEpoch = ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, tz).toInstant(); 

然后添加你的millis:

 Instant instant = modifiedEpoch.plusMillis(millis); 

跟踪日期时间的错误方法

首先,我必须说,在各个时区而不是UTC中使用count-from- epoch整数来表示日期时间值是一个非常非常糟糕的想法。 我已经看到了处理日期时间的一些不好的方法,包括自己发明一两种不好的方法。 但这个是最糟糕的。 想到这一点的人应该被判处一年的StackOverflow答案标记为“java”,“date”和“Jon Skeet”。

使用count-from-epoch处理应用程序代码中的日期时间就像使用位数组来处理文本一样。 我们有类/接口,如CharSequence,String,StringBuilder,Printer,Reader等,以处理文本,字符,字符编码,排序等的细节复杂的细节,以便我们更轻松地编写应用程序。 想象一下,尝试调试,排除故障并将文本数据记录为位数组。 疯了吧? 尝试调试,排除故障并记录日期时间数据,因为长整数也很疯狂。

同上日期时间,我们有Joda-Time ,现在已经在Java 8及更高版本中内置了java.time ( Tutorial )。

其次,隐含地将计数从一个时期调整到一个时区,然后失去这一事实使得糟糕的做法更加糟糕。

固定

解决这个问题的方法是将某个任意时区内的计数从一个时间段转换为本地日期和本地时间,其中本地意味着人们在时区看到的挂钟时间 。

使用本地日期时间,我们创建一个具有指定时区的日期时间对象ZonedDateTimeZonedDateTime基本上是Instant (UTC时间轴上的一个点)加上ZoneId (时区)。

由于该课题的作者未提供任何样本数据,让我们以这种棘手的方式创造一个价值。 获取芝加哥时区的当前时刻。 从纳秒级分辨率调整到毫秒 ,从而获得合法的计数器。 然后任意添加/减去该时区的UTC偏移量。

在这个例子中,我们使用America/Chicago时区。 我们的样本的偏移量为夏令时,为-05:00 。 以毫秒为单位,5 * 60 * 60 * 1,000 = 18,000,000。

  // First, create sample data, a count-from-epoch but not in UTC, instead adjusted for the time zone's offset. ZoneId zoneId = ZoneId.of( "America/Chicago" ); // 2015-09-19T12:34:56.000-05:00[America/Chicago] ZonedDateTime zdtTemp = ZonedDateTime.of( 2015 , 9 , 19 , 12 , 34 , 56 , 0 , zoneId ); long millisecondsFromEpoch = zdtTemp.toInstant().toEpochMilli(); // Loosing data, goin from nanosecond long offsetInMillisecondsForChicagoInDaylightSavingTime = 18_000_000L; // Offset of `-05:00` is in milliseconds, 5 * 60 * 60 * 1,000 = 18,000,000. long input = ( millisecondsFromEpoch - offsetInMillisecondsForChicagoInDaylightSavingTime ); 

转储到控制台。

 System.out.println( "zoneId : " + zoneId ); System.out.println( "zdtTemp : " + zdtTemp ); System.out.println( "millisecondsFromEpoch : " + millisecondsFromEpoch ); System.out.println( "offsetInMillisecondsForChicagoInDaylightSavingTime : " + offsetInMillisecondsForChicagoInDaylightSavingTime ); System.out.println( "input : " + input ); 

现在,做好这份工作。 拿这个棘手的输入数字,假装它是UTC,即使我们知道它不是,产生一个瞬发。 从Instant中获取LocalDateTime。 现在将LocalDateTime推送到时区以获得我们最终需要的ZonedDateTime

 // With example data in hand, proceed to convert to a valid date-time object. Instant instantPretendingToBeInUtcButNotReally = Instant.ofEpochMilli( input ); LocalDateTime localDateTimeOfPretendInstant = LocalDateTime.ofInstant( instantPretendingToBeInUtcButNotReally , ZoneOffset.UTC ); ZonedDateTime zdt = localDateTimeOfPretendInstant.atZone( zoneId ); 

转储到控制台。

 System.out.println( "instantPretendingToBeInUtcButNotReally : " + instantPretendingToBeInUtcButNotReally ); System.out.println( "localDateTimeOfPretendInstant : " + localDateTimeOfPretendInstant ); System.out.println( "zdt : " + zdt ); 

跑步的时候。

 zoneId : America/Chicago zdtTemp : 2015-09-19T12:34:56-05:00[America/Chicago] millisecondsFromEpoch : 1442684096000 offsetInMillisecondsForChicagoInDaylightSavingTime : 18000000 input : 1442666096000 instantPretendingToBeInUtcButNotReally : 2015-09-19T12:34:56Z localDateTimeOfPretendInstant : 2015-09-19T12:34:56 zdt : 2015-09-19T12:34:56-05:00[America/Chicago] 

CAVEAT我匆忙这样做了。 请评论或修正任何错误。

由于时间顺序时间单位是可互换的,乍一看似乎您可以使用以下双精度格式的本地日期时间:

 57272.5 

哪里…

  • 57272是从修改的朱利安日数纪元(1858年11月17日)算起的日期编号。
  • 0.5是当地时间,表示为一天的分数,例如当地时间0.5 =中午12:00。

以这种方式表达本地日期时间没有错。 但是,数字是数字而不是自修改的Julian天数纪元以来的天数,可以将此转换为毫秒数,因为POSIX纪元(看似)非常简单地如下:

 localMillis = ( dayNumber - POSIX_EPOCH_AS_MJD) / (86400.0 * 1000.0); 

在这种情况下,这就是“自当地时代以来毫秒”的概念。 但是,这里的错误是POSIX Epoch millis和“local”epoch millis之间没有简单的一对一对应关系(POSIX Time的定义要求计数是UTC中 Epoch的毫秒数)。 这是因为本地号码包含一个或多个来自UTC的本地偏移,这些偏移不能保证在历史上一致(取决于法规等)。

这些“本地”毫秒可以用作本地时间戳,但需要根据历史夏令时和时区偏移进行调整,与其他时间戳应该相同。 那么为什么要用呢? 我想不出一个理由。 有这种格式的时间戳是一个错误。

解决了这个问题:

  • 将“local millis”转换为修改后的Julian日数,并将当地时间表示为一天的分数
  • 将修改的朱利安天数转换为局部格里高利历日期和时间(算法改编自“星历算法”,第二版,由J.Meeus编写)。
  • 从上面获得的本地日历日期时间创建LocalDateTime实例
  • LocalDateTime与本地ZoneId以构造ZonedDateTime ,从中获取Instant
  • 从POSIX Epoch获得的POSIX时间为UTC毫秒,从Instant获得

此过程的代码示例如下:

 public static long epochMillisFromLocalMillis(ZoneId tz, long millis) { double dayNum = (millis / (86400.0 * 1000.0)) + POSIX_EPOCH_AS_MJD; int[] ymd_hmsm = toVectorFromDayNumber(dayNum); LocalDateTime ldt = LocalDateTime.of ( ymd_hmsm[MJD.YEAR], ymd_hmsm[MJD.MONTH], ymd_hmsm[MJD.DAY], ymd_hmsm[MJD.HOURS], ymd_hmsm[MJD.MINUTES], ymd_hmsm[MJD.SECONDS], ymd_hmsm[MJD.MILLIS] * 1000000); long utcMillis = ZonedDateTime .of(ldt, tz) .toInstant() .toEpochMilli(); return utcMillis; } 

感谢Basil Bourque和assylias对这个特殊问题的见解。