【问题标题】:How to parse LDAP Generalized Time in Java 8?如何在 Java 8 中解析 LDAP 通用时间?
【发布时间】:2017-02-20 00:50:44
【问题描述】:

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

这个定义有更多的复杂性,因为小数部分可以指小数小时、分钟或秒,具体取决于存在的内容。

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

DateTimeFormatter 的 API 接受/返回针对日期/时间抽象编写的类型,但它似乎实际上并不能与标准实现以外的任何东西一起使用,这非常令人失望。

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

有哪些选择?

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

【问题讨论】:

    标签: java datetime time ldap java-time


    【解决方案1】:

    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.2Z2017-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

    【讨论】:

      【解决方案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 时,默认忽略闰秒。

      【讨论】:

      • 发现闰秒后我们能做什么?假设我只想将 60 视为 59。我想我可以包装 TemporalAccessor 并将 SECOND_OF_MINUTE 作为特殊情况处理,然后使用 ZonedDateTime.from。一切似乎都还好,但创建DateTimeFormatter 却无法使用ZonedDateTime.parse 似乎有点奇怪。
      • 您可以将formatter.withResolverStyle(LENIENT) 与标准parse 方法一起使用。将会发生的是,23:59:60 将被视为下一个日期的 00:00,因此您需要减去一秒。
      猜你喜欢
      • 2017-02-13
      • 2015-06-26
      • 2017-09-26
      • 2012-09-29
      • 2021-07-17
      • 1970-01-01
      • 2015-01-06
      • 2021-11-05
      相关资源
      最近更新 更多