Java-8-approach的讨论:
闰秒处理
我不知道解析闰秒对您是否真的很重要(因为它是一个相当奇特的功能,在标准业务应用程序中几乎不发生),但我不建议使用标准 Java-8-API 来做它,另见官方documented limitations:
闰秒的处理仅限于
DateTimeFormatterBuilder.appendInstant()
因此,遵循直观的方法会失败(对于任何其他模式也是如此):
DateTimeFormatter dtf =
DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ssXXX").withResolverStyle(ResolverStyle.SMART);
TemporalAccessor raw = dtf.parse("2016-12-31T23:59:60Z");
Instant instant = Instant.from(raw);
System.out.println(
instant
+ " (leap-second-parsed=" + raw.query(DateTimeFormatter.parsedLeapSecond()) + ")");
你必须这样做:
DateTimeFormatter dtf =
DateTimeFormatter.ISO_INSTANT;
TemporalAccessor raw = dtf.parse("2016-12-31T23:59:60Z");
Instant instant = Instant.from(raw);
System.out.println(
instant
+ " (leap-second-parsed=" + raw.query(DateTimeFormatter.parsedLeapSecond()) + ")");
// 2016-12-31T23:59:59Z (leap-second-parsed=true)
但是,对于时区偏移不等于零的输入,它会失败,并且如果输入确实是真正的闰秒,由于内部缺少闰秒数据,代码不会验证,因为例如它说“2015-05-01T23:59:60Z”是闰秒(但我们知道它不是这样的)。
小数小时和分钟的处理
S. Colebourne(java.time-API 的作者)给出的建议解决方案存在缺陷。使用appendFraction() 只会处理一个元素,但处理小数部分需要以更高的精度处理指定元素和所有其他元素。先看打印示例(基于提案的代码):
DateTimeFormatter f = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd HH")
.appendFraction(ChronoField.MINUTE_OF_HOUR, 0, 6, true)
.appendOffsetId()
.toFormatter();
OffsetDateTime dt = OffsetDateTime.of(2017, 3, 21, 5, 28, 59, 0, ZoneOffset.UTC);
System.out.println(dt); // 2017-03-21T05:28:59Z
System.out.println(dt.format(f)); // 2017-03-21 05.466666Z
OffsetDateTime dt2 = OffsetDateTime.of(2017, 3, 21, 5, 28, 0, 0, ZoneOffset.UTC);
System.out.println(dt2); // 2017-03-21T05:28Z
System.out.println(dt2.format(f)); // 2017-03-21 05.466666Z
我们看到两个不同的OffsetDateTime-values 导致相同的十进制小时,这显然是错误的。差异只是SECOND_OF_MINUTE 字段中的一个增量(appendFraction() 未考虑在内)。
解析呢?我们可以反过来观察到相同的效果,这使得整个方法无法使用。
让输入解析为“2017-03-01 13.52Z”,如提案中给出的示例。观察到的解析值是:2017-03-01T13:31Z 但是这个结果是不正确的。它应该是:2017-03-01T13:31.2Z 或 2017-03-01T13:31:12Z(解释:0.52 x 60 = 31.2 => 分钟分量和 0.2 * 60 = 12 => 秒分量)。
结论:不要使用标准 API 来处理与时间相关的元素的十进制值。没有设计支持。我说即使是“设计使然”,因为所有字段最终都使用长基元(作为值类型),它不适合合并基于多个字段的十进制值。
还能做什么?我已经设置了my own library 来填补上面描述的 java.time-API 中的空白。
Time4J 解决方案(v4.25 或更高版本):
我建议您使用以下代码来模拟 LDAP 规范。它相当复杂,但由于规范本身的复杂性,这是必要的。
ChronoFormatter<PlainDate> df =
ChronoFormatter.setUp(PlainDate.axis(), Locale.ROOT)
.addFixedInteger(PlainDate.YEAR, 4)
.addFixedInteger(PlainDate.MONTH_AS_NUMBER, 2)
.addFixedInteger(PlainDate.DAY_OF_MONTH, 2)
.build();
ChronoFormatter<Moment> mf =
ChronoFormatter.setUp(Moment.axis(), Locale.US) // US for preference of dot in decimal elements
.addCustomized(PlainDate.COMPONENT, df)
.addFixedInteger(PlainTime.DIGITAL_HOUR_OF_DAY, 2)
.startOptionalSection()
.addFixedInteger(PlainTime.MINUTE_OF_HOUR, 2)
.startOptionalSection()
.addFixedInteger(PlainTime.SECOND_OF_MINUTE, 2)
.startOptionalSection()
.addLiteral('.', ',')
.addFraction(PlainTime.NANO_OF_SECOND, 1, 9, false)
.endSection()
.endSection()
.endSection()
.addTimezoneOffset(DisplayMode.SHORT, false, Collections.singletonList("Z"))
.or()
.addCustomized(PlainDate.COMPONENT, df)
.addFixedInteger(PlainTime.DIGITAL_HOUR_OF_DAY, 2)
.addFixedDecimal(PlainTime.DECIMAL_MINUTE)
.addTimezoneOffset(DisplayMode.SHORT, false, Collections.singletonList("Z"))
.or()
.addCustomized(PlainDate.COMPONENT, df)
.addFixedDecimal(PlainTime.DECIMAL_HOUR)
.addTimezoneOffset(DisplayMode.SHORT, false, Collections.singletonList("Z"))
.build();
assertThat(
mf.parse("199412160532-0500").toString(),
is("1994-12-16T10:32:00Z"));
assertThat(
mf.parse("199412160532Z").toString(),
is("1994-12-16T05:32:00Z"));
assertThat(
mf.parse("20161231185960.123456789-0500").toString(),
is("2016-12-31T23:59:60,123456789Z"));
assertThat(
mf.parse("201612311859.25-0500").toString(),
is("2016-12-31T23:59:15Z"));
assertThat(
mf.parse("2016123118.25-0500").toString(),
is("2016-12-31T23:15:00Z"));
如您所见,代码确实处理了闰秒(即使偏移量不为零)。 Time4J 还验证闰秒,因为它管理其独立的闰秒数据(例如从 IANA-TZDB 中提取)。闰秒存储在Moment 类型的对象中。此类型与java.time.Instant 对应。 conversion between both types 是微不足道的(或直接通过方法 moment.toTemporalAccessor())。请注意:在这种转换过程中,闰秒本身将丢失。如果您只想忽略闰秒,即像前一秒一样处理它,那么要么转换为Instant,要么使用Moment 中的标准POSIX 相关方法(以及转换为任何“本地”类型像PlainTimestamp/LocalDateTime 等也失去了闰秒)。
还支持十进制值,因为接口ChronoElement(与TemporalField 对应)是通用的,并且基于对象值类型,而不是长原语,例如参见decimal minute 的元素,它使用值类型BigDecimal。
最后解析点或逗号是可能的(根据 LDAP 规范的要求)。这是 Java-8 也不支持的细节,比较参见JDK-issue 8163932。