Steve Ebersole
Steve Ebersole

Reputation: 9443

Conditionally create LocalDateTime, ZonedDateTime or OffsetDateTime based on parsed TemporalAccessor

Given a DateTimeFormatter defined as:

public static final DateTimeFormatter DATE_TIME = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .append( ISO_LOCAL_DATE )
        .optionalStart().appendLiteral( ' ' ).optionalEnd()
        .optionalStart().appendLiteral( 'T' ).optionalEnd()
        .append( ISO_LOCAL_TIME )
        .optionalStart().appendLiteral( ' ' ).optionalEnd()
        .optionalStart().appendZoneOrOffsetId().optionalEnd()
        .toFormatter();

I'd like to conditionally create a LocalDateTime, ZonedDateTime or OffsetDateTime based on whether the parsing included a zone-id, an offset or neither.

So far I have:

    DATE_TIME.parse(
            text,
            (temporal) -> {
                // see if there is an offset
                final ZoneOffset offset = temporal.query( TemporalQueries.offset() );
                if ( offset != null ) {
                    return OffsetDateTime.from( temporal );
                }

                // see if there is a tz
                final ZoneId zoneId = temporal.query( TemporalQueries.zoneId() );
                if ( zoneId != null ) {
                    return ZonedDateTime.from( temporal );
                }

                // otherwise its a LocalDateTime
                return LocalDateTime.from( temporal );
            }
    );

What I have found though is that a zone-offset is never "recognized" - even if the text includes an offset, it is always reported as a zone-id. E.g. given "1999-12-31 12:59:59 +02:00" I'd expect an OffsetDateTime. However the "+02:00" is always parsed as a zone-id. Ultimately it works given the reciprocity between zoned and offset. But as a matter of (probably way over the top) correctness I'd like to treat these as OffsetDateTime.

Am I missing something to be able to make that distinction?

Thanks!

Upvotes: 2

Views: 283

Answers (2)

Anonymous
Anonymous

Reputation: 86323

This is not really an answer. Jacob G. already provided that. I’d just like to supplement.

Workaround for Java 8

The ZoneId that you get from either TemporalQueries.zone() orTemporalQueries.zoneId() is a ZoneOffset when an offset has been parsed from the string. EDIT: UTC+02:00 doesn’t come out as a ZoneOffset, but calling normalized() on the ZoneId converts it into one. Now the instanceof operator will tell you which you have got.

    for (String text : new String[] {
            "1999-12-31 12:59:59",
            "1999-12-31 12:59:59 +02:00",
            "1999-12-31 12:59:59 UTC+02:00",
            "1999-12-31 12:59:59 America/Porto_Velho",
    }) {
        TemporalAccessor parsed = DATE_TIME.parse(
                text ,
                (temporal) -> {
                    // see if there is an offset or tz
                    final ZoneId zoneOrOffset = temporal.query( TemporalQueries.zone() );
                    if ( zoneOrOffset != null ) {
                        ZonedDateTime zdt = ZonedDateTime.from( temporal );
                        // EDIT: call normalized() to convert a ZoneId
                        // with constant offset, e.g., UTC+02:00, to ZoneOffset
                        if (zoneOrOffset.normalized() instanceof ZoneOffset) {
                            return zdt.toOffsetDateTime();
                        } else {
                            return zdt;
                        }
                    }

                    // otherwise it's a LocalDateTime
                    return LocalDateTime.from( temporal );
                }
        );

        System.out.format(Locale.ENGLISH, "%-30s %s%n", parsed.getClass(), parsed);
    }

Output from this snippet is:

class java.time.LocalDateTime  1999-12-31T12:59:59
class java.time.OffsetDateTime 1999-12-31T12:59:59+02:00
class java.time.OffsetDateTime 1999-12-31T12:59:59+02:00
class java.time.ZonedDateTime  1999-12-31T12:59:59-04:00[America/Porto_Velho]

Simpler way for Java 9 and later

You were working too hard. Using DateTimeFormatter.parseBest() ought to have done your job in Java 8 and does work in Java 9 and later.

    String text = "1999-12-31 12:59:59 +02:00";

    TemporalAccessor parsed = DATE_TIME.parseBest(text,
            OffsetDateTime::from, ZonedDateTime::from, LocalDateTime::from);

    System.out.println(parsed.getClass());
    System.out.println(parsed);

Output on Java 9:

class java.time.OffsetDateTime
1999-12-31T12:59:59+02:00

EDIT: This way doesn’t make an OFfsetDateTime out of a string with UTC+02:00 as time zone, though.

Upvotes: 1

Jacob G.
Jacob G.

Reputation: 29710

It looks like this was actually a bug in Java 8. If you store the value of DateTimeFormatter#parse in a Temporal variable and print its getClass(), you receive the following when compiling and running with Java 8:

class java.time.ZonedDateTime

However, when compiling and running with Java 11, the output is what you expect:

class java.time.OffsetDateTime

I'm going to search around for the specific bug report and edit this answer if I manage to find it. That way, we can determine the cause of the bug as well as the version of Java it was fixed in.

Upvotes: 2

Related Questions