在 Java 中将纪元转换为 ZonedDateTime

converting epoch to ZonedDateTime in Java

如何将 1413225446.92000 中的纪元转换为 java 中的 ZonedDateTime

给定的代码需要 long 值,因此这将为上面给定的值抛出 NumberFormatException

ZonedDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(dateInMillis)), ZoneId.of(TIME_ZONE_PST));

将数字拆分为一对 64 位长整数:

  • UTC 中自 1970 年第一个时刻的纪元参考日期以来的整秒数
  • 小数秒的纳秒数

将这些数字传递给工厂方法Instant.ofEpochSecond​(long epochSecond, long nanoAdjustment)

手头有 Instant,继续指定时区以获得 ZonedDateTime

ZoneId z = ZoneId.of( "America/Los_Angeles" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;

java.time可以直接解析你的字符串

编辑:如果你的毫秒值总是非负的,下面的DateTimeFormatter可以解析它。

private static final String TIME_ZONE_PST = "America/Los_Angeles";
private static final DateTimeFormatter epochFormatter = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NEVER)
        .optionalStart()
        .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
        .optionalEnd()
        .toFormatter()
        .withZone(ZoneId.of(TIME_ZONE_PST));

现在解析成 ZonedDateTime 只是一个方法调用:

    ZonedDateTime zdt = ZonedDateTime.parse(dateInMillis, epochFormatter);
    System.out.println(zdt);

输出为:

2014-10-13T11:37:26.920-07:00[America/Los_Angeles]

负值无法正常工作:分数仍会被解析为正数,我认为这是不正确的。为了确保在负值的情况下得到通知,我在格式化程序中指定了数字无法签名。

更通用的解决方案:使用 BigDecimal

如果您需要更通用的解决方案,例如包括负数,我认为最好让 BigDecinmal 解析数字并进行数学运算。

    BigDecimal bd = new BigDecimal(dateInMillis);
    BigDecimal[] wholeAndFractional = bd.divideAndRemainder(BigDecimal.ONE);
    long seconds = wholeAndFractional[0].longValueExact();
    int nanos = wholeAndFractional[1].movePointRight(9).intValue();
    ZonedDateTime zdt = Instant.ofEpochSecond(seconds, nanos)
            .atZone(ZoneId.of(TIME_ZONE_PST));

输出与之前相同。只是现在我们也可以按照预期来处理负数了:

    String dateInMillis = "-1.5";

1969-12-31T15:59:58.500-08:00[America/Los_Angeles]

甚至接受科学记数法:

    String dateInMillis = "1.41322544692E9";

2014-10-13T11:37:26.920-07:00[America/Los_Angeles]

如果字符串的精度可能高于纳秒,请考虑如何截断或舍入,并相应地指示 BigDecimal,有多种选择。

原回答

不错。将纳秒从小数部分取出为纳秒的整数可能会导致一两个陷阱。我建议:

    String dateInMillis = "1413225446.92000";
    String[] secondsAndFraction = dateInMillis.split("\.");
    int nanos = 0;
    if (secondsAndFraction.length > 1) { // there’s a fractional part
        // extend fractional part to 9 digits to obtain nanoseconds
        String nanosecondsString
                = (secondsAndFraction[1] + "000000000").substring(0, 9);
        nanos = Integer.parseInt(nanosecondsString);
        // if the double number was negative, the nanos must be too
        if (dateInMillis.startsWith("-")) {
            nanos = -nanos;
        } 
    }
    ZonedDateTime zdt = Instant
            .ofEpochSecond(Long.parseLong(secondsAndFraction[0]), nanos)
            .atZone(ZoneId.of("Asia/Manila"));
    System.out.println(zdt);

这会打印

2014-10-14T02:37:26.920+08:00[Asia/Manila]

纳秒不需要 64 位,所以我只使用 int

假设:我假设你的字符串包含一个浮点数并且它可能是有符号的,例如 -1.50 意味着一秒半 before[=61= 】 纪元。如果有一天你的纪元时间以科学计数形式出现(1.41322544692E9),上面的方法将不起作用。

如果不是 Asia/Manila,请用 region/city 格式替换您想要的时区,例如 America/Vancouver , America/Los_Angeles 或 Pacific/Pitcairn。避免像 PST 这样的三个字母缩写,它们是模棱两可的,而且通常不是真正的时区。

在这里扩展 Basil 和 Ole 的答案,针对 negative 时间戳的特殊情况,即在纪元之前。这可能吗?这是 Jon Skeet 在 "All about java.util.Date" 中写的内容:

The Date class uses “milliseconds since the Unix epoch” – that’s the value returned by getTime(), and set by either the Date(long) constructor or the setTime() method. As the moon walk occurred before the Unix epoch, the value is negative: it’s actually -14159020000.

Ole 的答案(除了一些额外的断言)之间唯一真正的区别是,在这里,如果日期字符串以负号开头,我们不会反转 nanos 上的符号。这样做的原因是,当将 nanos 传递给 Instant constructor 时,这是一个 调整 ,所以如果我们将 nanos 作为负数发送,它实际上会将秒数调整回来,因此整个 ZonedDateTime 值被纳米级关闭。

这是来自 JavaDoc,请注意有趣的行为:

This method allows an arbitrary number of nanoseconds to be passed in. The factory will alter the values of the second and nanosecond in order to ensure that the stored nanosecond is in the range 0 to 999,999,999. For example, the following will result in the exactly the same instant:

Instant.ofEpochSecond(3, 1);
Instant.ofEpochSecond(4,-999_999_999);
Instant.ofEpochSecond(2, 1000_000_001);

所以第二个参数,nanos,我们不是在设置值,它是一个调整。因此,与正时间戳(纪元之后)一样,我们要发送实际的纳米。

以Ole代码为基础,加入上述改动:

    String strDateZoned = "Jul 20 1969 21:56:20.599 CDT"; // yes, should use America/Chicago here as Ole points out
    DateTimeFormatter dtfFormatter  = DateTimeFormatter.ofPattern("MMM dd yyyy HH:mm:ss.SSS zzz");
    ZonedDateTime     originalZoned  = ZonedDateTime.parse(strDateZoned, dtfFormatter);

    long epochSeconds = originalZoned.toInstant().getEpochSecond();
    int nanoSeconds = originalZoned.toInstant().getNano();
    String dateInMillis = epochSeconds + "." + nanoSeconds;
    String[] secondsAndFraction = dateInMillis.split("\.");
    int nanos = 0;
    if (secondsAndFraction.length > 1) { // there’s a fractional part
        // extend fractional part to 9 digits to obtain nanoseconds
        String nanosecondsString
                = (secondsAndFraction[1] + "000000000").substring(0, 9);
        nanos = Integer.parseInt(nanosecondsString);
    }

    ZonedDateTime zdt = Instant
            .ofEpochSecond(Long.parseLong(secondsAndFraction[0]), nanos)
            .atZone(ZoneId.of("America/Chicago"));

    String formattedZdt = dtfFormatter.format(zdt);

    System.out.println("zoneDateTime expected    = " + strDateZoned);
    System.out.println("zoneDateTime from millis = " + formattedZdt);

    assertEquals("date in millis is wrong",  "-14159020.599000000", dateInMillis);
    assertEquals("date doesn't match expected",strDateZoned, dtfFormatter.format(zdt));

代码输出:

zoneDateTime expected    = Jul 20 1969 21:56:20.599 CDT
zoneDateTime from millis = Jul 20 1969 21:56:20.599 CDT

如果我们在秒部分为负的情况下反转 nanos 的符号,我们可以看到格式化的 ZonedDateTime 中的差异:

org.junit.ComparisonFailure: date doesn't match expected 
Expected :Jul 20 1969 21:56:20.599 CDT
Actual   :Jul 20 1969 21:56:19.401 CDT

P.S。 'All About Dates' post 关于 Jon Skeet 所谓的 "leniency" 以及我在其他地方看到的 'normalization' 的一些想法,这可能是由于 POSIX 的影响:

It’s lenient for no obvious reason: “In all cases, arguments given to methods for these purposes need not fall within the indicated ranges; for example, a date may be specified as January 32 and is interpreted as meaning February 1.” How often is that useful?