Darnoj
Darnoj

Reputation: 209

Parsing and formatting LocalDate with unnecessary time and timezone

Edit :

I opened a bug and it has been confirmed by Oracle. You can follow the resolution here : https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8216414


I'm interfacing with a LDAP repository which store the birthdate of a person with the time and timezone like this :

I cannot find a way to parse AND format the birthdate using the same pattern.

The following code works well for formatting but not for parsing :

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);

And the following code works well for parsing but not for formatting

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);

Which pattern could satisfy both parsing and formating ?

Am I forced to use 2 different patterns ?

I am asking because the pattern is configured in a property file. I want to configure 1 pattern only in this property file. I would like to externalize the pattern because the LDAP is not part of my project, it is a shared resource and I have no guarantee that the format cannot change.

Upvotes: 6

Views: 5714

Answers (3)

Anonymous
Anonymous

Reputation: 86306

I suggest:

    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);

The LDAP string represents both date, time and UTC offset. The good solution is to respect that and generate all of those when formatting (setting time of day to 00:00 and offset to 0) and parsing all of them back (at best also validating them to catch if any surprises should arise). Conversion between LocalDate and OffsetDateTime is straightforward when you know how.

Edit 3: Allowing the pattern to be configured

… 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.

To take the possibility into account that the pattern may some day not contain time of day and/or no UTC offset use this formatter in the above code:

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

This defines a default time of day (midnight) and a default offset (0). As long as time and offset are defined in the string from LDAP, the defaults are not used.

If you think it is getting too complicated, using two configured formats, one for formatting and one for parsing, may be the best solution (the least annoying solution) for you.

Edit: Avoiding type conversions

I consider the above the nice solution. However, if you insist an avoiding the conversion from LocalDate to ZonedDateTime using atStartOfDay and from OffsetDateTime using toLocalDate, that is possible through the following 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);

I am specifying the exact width of each field so that the formatter can know where to separate them in the string when parsing.

Edit 2: Is this a bug in parsing?

I would immediately have expected yyyyMMdd'000000+0000' to work for both formatting and parsing. You may try filing a bug with Oracle and seeing what they say, though I wouldn’t bee too optimistic.

Upvotes: 1

ETO
ETO

Reputation: 7279

Since your LDAP string has zoned format ...+0000, I would suggest using ZonedDateTime or OffsetDateTime.

This pattern yyyyMMddHHmmssZZZ would do the trick for both parsing and formatting.

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

Formatting

  • First convert your LocalDate to ZonedDateTime/OffsetDateTime:

    ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneOffset.UTC);
    // or
    OffsetDateTime offsetDateTime = date.atStartOfDay().atOffset(ZoneOffset.UTC);
    
  • Then format it:

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

Parsing

  • First parse the ZonedDateTime/OffsetDateTime:

    // Both parse correctly
    ZonedDateTime zonedDateTime = ZonedDateTime.parse("20181227000000+0000", formatter);
    // or
    OffsetDateTime offsetDateTime = OffsetDateTime.parse("20181227000000+0000", formatter);
    
  • Once you have ZonedDateTime/OffsetDateTime, you can simply retrieve the LocalDate like this:

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

Update

Both parsing and formatting can be simplified to one-liners:

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

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

In case you're still unsatisfied with the code above then you can extract the logic to utility methods:

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);
}

Upvotes: 3

JB Nizet
JB Nizet

Reputation: 691845

Stupid simple solution:

    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));

Upvotes: 1

Related Questions