如何在Java 8中解析LDAP广义时间?

时间:2017-02-20 00:50:45

标签: java datetime time ldap java-time

轻量级目录访问协议(LDAP):语法和匹配规则定义Generalized Time,其中的秒组件可能是闰秒。

定义有更多复杂因素,因为分数成分可能指的是分数小时,分钟或秒,具体取决于存在的内容。

我尝试过使用DateTimeFormatterBuilder的各种方法,例如appendInstantparseUnresolved,甚至自定义TemporalField

DateTimeFormatter的API采用/返回针对日期/时间抽象的类型,但它似乎并没有实际使用除标准实现之外的其他任何东西,这是非常令人失望的。

当然其他人也有类似的自定义格式,我希望这些用例被视为JSR-310的一部分。

有哪些选择?

是否可以创建一个可以重用大多数现有ISO8601解析逻辑的自定义格式化程序?

2 个答案:

答案 0 :(得分:2)

此代码解析小时数。它可以调整为分数天:

DateTimeFormatter f = new DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM-dd HH")
    .appendFraction(MINUTE_OF_HOUR, 0, 6, true)
    .appendOffsetId()
    .toFormatter();
OffsetDateTime dt = OffsetDateTime.now();
System.out.println(dt.format(f));
System.out.println(OffsetDateTime.parse("2017-03-01 13.52Z", f));

此代码可用于查找闰秒:

DateTimeFormatter fmt = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
String text = "2017-03-01T23:59:60";
ParsePosition pp = new ParsePosition(0);
TemporalAccessor accessor = fmt.parseUnresolved(text, pp);
if (pp.getErrorIndex() >= 0) {
  throw new DateTimeParseException("Parse error", text, pp.getErrorIndex());
}
if (accessor.getLong(SECOND_OF_MINUTE) == 60) {
  System.out.println("Leap second");
} else {
  System.out.println("Not a leap second");
}

时间库非常易于扩展。 ThreeTen-Extra处的实施显示了可以根据YearQuarter等备用日期时间类和PackedFields等备用字段执行的操作。

更新

值得注意的是后人的复杂性来自于需要支持小数分钟,小时和天。解析标准Instant时,默认情况下只会忽略闰秒。

答案 1 :(得分:2)

讨论Java-8方法:

处理闰秒

我不知道解析闰秒对你来说是否真的很重要(因为它是一种在标准业务应用程序中很难发生的奇特功能),但我不建议使用标准的Java-8-API为此,请参见官方documented limitations

  

闰秒的处理仅限于   DateTimeFormatterBuilder.appendInstant()

因此,遵循直观的方法失败(并且对任何其他模式都这样做):

class Array 
     def search(string) 
        for element in self
          if element.include? (string)
            yield(element)
          end
        end
     end 
end

list.search("an"){|i| puts i}

相反,你必须这样做:

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) "是闰秒(但我们知道它不是这样的)。

处理小数小时和分钟

S. Colebourne(java.time-API的作者)给出的建议解决方案存在缺陷。使用2015-05-01T23:59:60Z只会处理一个元素,但处理小数部分需要处理指定的元素和所有其他具有更高精度的元素。首先查看打印示例(基于提案的代码):

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中的差异(SECOND_OF_MINUTE未考虑)。

解析怎么样?我们可以反过来观察相同的效果,这使整个方法无法使用。

让输入被解析" 2017-03-01 13.52Z"如提案中给出的那样。观察到的解析值为:appendFraction()但是此结果不正确。它应该是:2017-03-01T13:31Z2017-03-01T13:31.2Z(解释:0.52 x 60 = 31.2 =>分钟组件和0.2 * 60 = 12 =>第二组件)。

结论:不要使用标准API来处理与时间相关的元素的十进制值。没有设计支持。我甚至说"设计"因为所有字段最终都使用long-primitive(作为值类型),不适合基于多个字段合并十进制值。

该怎么办?我已经设置了my own library来填补java.time-API中的空白,如上所述。

Time4J-solution(v4.25或更高版本):

我建议您使用以下代码来建模LDAP规范。它相当复杂,但由于规范本身的复杂性,这是必要的。

2017-03-01T13:31:12Z

如您所见,代码确实处理闰秒(即使具有非零偏移)。 Time4J还验证闰秒,因为它管理其独立的闰秒数据(例如从IANA-TZDB中提取)。闰秒存储在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")); 类型的对象中。此类型与Moment对应。 conversion between both types是微不足道的(或直接通过方法java.time.Instant)。需要注意的是:在这种转换过程中,闰秒本身就会丢失。如果您只是想忽略闰秒,即在最后一秒之前处理它,或者只需转换到moment.toTemporalAccessor()或使用Instant中的标准POSIX相关方法(以及转换为任何方法) &#34;本地&#34;类似Moment等类型也失去了闰秒。)

也支持十进制值,因为接口PlainTimestamp/LocalDateTime(与ChronoElement的对应)是基于对象值类型而不是long-primitive,参见例如{的元素{3}}使用值类型TemporalField

最后解析点或逗号是可能的(根据LDAP规范的要求)。这是Java-8也不支持的细节,为了进行比较,请参阅decimal minute