使用不必要的时间和时区解析和格式化 LocalDate

Parsing and formatting LocalDate with unnecessary time and timezone

编辑:

我开了一个bug,已经被Oracle确认了。您可以按照此处的决议:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8216414


我正在与一个 LDAP 存储库连接,该存储库存储一个人的生日以及这样的时间和时区:

我找不到使用相同模式解析和格式化生日的方法

以下代码适用于格式化但不适用于解析:

LocalDate date = LocalDate.of(2018, 12, 27);
String pattern = "yyyyMMdd'000000+0000'";
DateTimeFormatter birthdateFormat = DateTimeFormatter.ofPattern(pattern);

// Outputs correctly 20181227000000+0000
date.format(birthdateFormat);

// Throw a DatetimeParseException at index 0
date = LocalDate.parse("20181227000000+0000", birthdateFormat);

下面的代码可以很好地解析但不适用于格式化

LocalDate date = LocalDate.of(2018, 12, 27);
String pattern = "yyyyMMddkkmmssxx";
DateTimeFormatter birthdateFormat = DateTimeFormatter.ofPattern(pattern);

// Throws a UnsupportedTemporalTypeException for ClockHourOfDay not supported
// Anyway I would have an unwanted string with non zero hour, minute, second, timezone
date.format(birthdateFormat);

// Parse correctly the date to 27-12-2018
date = LocalDate.parse("20181227000000+0000", birthdateFormat);

哪种模式可以同时满足解析和格式化?

我是否被迫使用 2 种不同的模式?

我问是因为模式是在 属性 文件中配置的。我只想在此 属性 文件中配置 1 个模式。我想将模式外部化,因为 LDAP 不是我项目的一部分,它是一个共享资源,我不能保证格式不会改变。

愚蠢的简单解决方案:

    String s1 = "20181227000000+0000";
    DateTimeFormatter yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd");
    LocalDate date = LocalDate.parse(s1.substring(0, 8), yyyyMMdd);
    System.out.println("date = " + date);
    String s2 = date.format(yyyyMMdd) + "000000+0000";
    System.out.println("s2 = " + s2);
    System.out.println(s1.equals(s2));

由于您的 LDAP 字符串具有分区格式 ...+0000,我建议使用 ZonedDateTimeOffsetDateTime

此模式 yyyyMMddHHmmssZZZ 可以同时用于解析和格式化。

LocalDate date =  LocalDate.of(2018, 12, 27);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssZZZ");

格式化

  • 首先将你的LocalDate转换为ZonedDateTime/OffsetDateTime:

    ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneOffset.UTC);
    // or
    OffsetDateTime offsetDateTime = date.atStartOfDay().atOffset(ZoneOffset.UTC);
    
  • 然后格式化:

    // Both output correctly 20181227000000+0000
    System.out.println(zonedDateTime.format(formatter));
    // or 
    System.out.println(offsetDateTime.format(formatter));
    

正在解析

  • 首先解析ZonedDateTime/OffsetDateTime:

    // Both parse correctly
    ZonedDateTime zonedDateTime = ZonedDateTime.parse("20181227000000+0000", formatter);
    // or
    OffsetDateTime offsetDateTime = OffsetDateTime.parse("20181227000000+0000", formatter);
    
  • 一旦你有 ZonedDateTime/OffsetDateTime,你可以像这样简单地检索 LocalDate

    LocalDate date = LocalDate.from(zonedDateTime);
    // or
    LocalDate date = LocalDate.from(offsetDateTime);
    

更新

解析格式化都可以简化为一行:

LocalDate date = LocalDate.from(formatter.parse(ldapString));

String ldapString = OffsetDateTime.of(date, LocalTime.MIN, ZoneOffset.UTC).format(formatter);

如果您对上面的代码仍然不满意,那么您可以将逻辑提取到实用方法中:

public LocalDate parseLocalDate(String ldapString) {
    return LocalDate.from(formatter.parse(ldapString));
}

public String formatLocalDate(LocalDate date) {
    return OffsetDateTime.of(date, LocalTime.MIN, ZoneOffset.UTC)
                         .format(formatter);
}

我建议:

    LocalDate date = LocalDate.of(2018, Month.DECEMBER, 27);
    String pattern = "yyyyMMddHHmmssxx";
    DateTimeFormatter birthdateFormat = DateTimeFormatter.ofPattern(pattern);

    // Outputs 20181227000000+0000
    String formatted = date.atStartOfDay(ZoneOffset.UTC).format(birthdateFormat);
    System.out.println(formatted);

    // Parses to 2018-12-27T00:00Z
    OffsetDateTime odt = OffsetDateTime.parse("20181227000000+0000", birthdateFormat);
    System.out.println(odt);
    // Validate
    if (! odt.toLocalTime().equals(LocalTime.MIN)) {
        System.out.println("Unexpected time of day: " + odt);
    }
    if (! odt.getOffset().equals(ZoneOffset.UTC)) {
        System.out.println("Unexpected time zone offset: " + odt);
    }
    // Converts to 2018-12-27
    date = odt.toLocalDate();
    System.out.println(date);

LDAP 字符串表示日期、时间和 UTC 偏移量。好的解决方案是尊重这一点并在格式化时生成所有这些(将一天中的时间设置为 00:00 并将偏移量设置为 0)并解析所有这些(最好也验证它们以捕捉是否出现任何意外) .如果您知道如何进行 LocalDateOffsetDateTime 之间的转换,那就很简单了。

编辑 3:允许配置模式

… the pattern is configured in a property file… I want to configure 1 pattern only in this property file.

… I have no guarantee that the format cannot change.

考虑到模式可能某一天不包含一天中的时间的可能性and/or没有UTC偏移量在上面的代码中使用这个格式化程序:

    DateTimeFormatter birthdateFormat = new DateTimeFormatterBuilder()
            .appendPattern(pattern)
            .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
            .toFormatter()
            .withZone(ZoneOffset.UTC);

这定义了一天中的默认时间(午夜)和默认偏移量 (0)。只要在来自 LDAP 的字符串中定义了时间和偏移量,就不会使用默认值。

如果你觉得它变得太复杂,使用两种配置的格式,一种用于格式化,一种用于解析,可能是最适合你的解决方案(最不烦人的解决方案)。

编辑:避免类型转换

我认为上面的解决方案很好。但是,如果您坚持使用 atStartOfDay 避免从 LocalDateZonedDateTime 的转换以及使用 toLocalDateOffsetDateTime 的转换,则可以通过以下 hack:

    DateTimeFormatter birthdateFormat = new DateTimeFormatterBuilder()
            .appendValue(ChronoField.YEAR, 4, 4, SignStyle.NEVER)
            .appendValue(ChronoField.MONTH_OF_YEAR, 2, 2, SignStyle.NEVER)
            .appendValue(ChronoField.DAY_OF_MONTH, 2, 2, SignStyle.NEVER)
            .appendLiteral("000000+0000")
            .toFormatter();

    // Outputs 20181227000000+0000
    String formatted = date.format(birthdateFormat);
    System.out.println(formatted);

    // Parses into 2018-12-27
    date = LocalDate.parse("20181227000000+0000", birthdateFormat);
    System.out.println(date);

我正在指定每个字段的确切宽度,以便格式化程序在解析时可以知道在字符串中的何处分隔它们。

编辑 2:这是解析中的错误吗?

我会立即期望 yyyyMMdd'000000+0000' 可以用于格式化和解析。您可以尝试向 Oracle 提交错误,看看他们怎么说,但我不会太乐观。