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

How do I get POSIX time (UTC) from a serial value local date-time with the Java 8 DateTime API

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

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

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

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

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


计算修改时期的瞬间:

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

然后添加你的millis:

1
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)。

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

固定

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

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

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

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

1
2
3
4
5
6
7
8
    // 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 );

转储到控制台。

1
2
3
4
5
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

1
2
3
4
// 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 );

转储到控制台。

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

跑步时

1
2
3
4
5
6
7
8
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我匆忙做到了这一点。请评论或修正任何错误。


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

1
57272.5

哪里...

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

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

1
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获得

此过程的代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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对这个特殊问题的见解。