2

I have the below program and looks like ZonedDateTime is not able to parse the date string. Should I use a different date format or different library to parse?

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

class Scratch {
    public static void main(String[] args) {
        final String inputDate = "2022-03-12T03:59:59+0000Z";
        ZonedDateTime.parse(inputDate, DateTimeFormatter.ISO_DATE_TIME).toEpochSecond();
    }
}

Exception in thread "main" java.time.format.DateTimeParseException: Text '2022-03-12T03:59:59+0000Z' could not be parsed, unparsed text found at index 19
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2053)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1952)
    at java.base/java.time.ZonedDateTime.parse(ZonedDateTime.java:599)
    at Scratch.main(scratch_29.java:7)

Process finished with exit code 1
VGR
  • 40,506
  • 4
  • 48
  • 63
Zack
  • 2,078
  • 10
  • 33
  • 58
  • Does the string always end in `+0000Z` literally? As has been said, it’s funny and non-standard. Could you educate the publisher to give you true [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format? – Ole V.V. Mar 15 '22 at 08:41
  • If you can’t change the string, I’d declare `private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE_TIME).appendOffset("+HHMM", "+0000").appendOffsetId().toFormatter(Locale.ROOT);` (sorry, unreadable in the comment, but simpler than the one by stdunbar). And I’d prefer to parse into an `OffsetDateTime` over`ZonedDateTime` (both work). – Ole V.V. Mar 15 '22 at 08:46

2 Answers2

2

That isn't a ISO_DATE_TIME format. That would require something like 2022-03-12T03:59:59+0000 (no 'Z'). A formatter that works looks something like:

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;


class Scratch {
    public static void main(String[] args) {
        final String inputDate = "2022-03-12T03:59:59+0000Z";

        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                .parseCaseInsensitive()
                .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
                .optionalStart()
                .appendPattern(".SSS")
                .optionalEnd()
                .optionalStart()
                .appendZoneOrOffsetId()
                .optionalEnd()
                .optionalStart()
                .appendOffset("+HHMM", "0000")
                .optionalEnd()
                .optionalStart()
                .appendLiteral('Z')
                .optionalEnd()
                .toFormatter();

        long epochSecond = ZonedDateTime.parse(inputDate, formatter).toEpochSecond();

        System.out.println("epochSecond is " + epochSecond);
    }
}

as derived from this post. You can create that formatter in one place and use it over again.

stdunbar
  • 16,263
  • 11
  • 31
  • 53
2

tl;dr

Your input happens to be a screwy variation of the standard IS0 8601 format used by default with the java.time.Instant class. Fix your input string, and parse.

Instant.parse( "2022-03-12T03:59:59+0000Z".replace( "+0000Z" , "Z" ) )

Or, reformatted:

Instant.parse
( 
    "2022-03-12T03:59:59+0000Z"
    .replace( "+0000Z" , "Z" ) 
)

See this code run live at IdeOne.com.

2022-03-12T03:59:59Z

But the very best solution is to use only ISO 8601 formats when exchanging date-time values.

Details

ZonedDateTime inappropriate here

ZonedDateTime is the wrong class for your input. Your input has a Z which is a standard abbreviation for an offset from UTC of zero hours-minutes-seconds. But no time zone indicated, only a mere offset. So use OffsetDateTime or Instant rather than ZonedDateTime.

+0000 and Z redundant

The +0000 also means an offset of zero hours-minutes-seconds. This is the same meaning as the Z. So this part is redundant. I suggest you educate the publisher of your data about the standard ISO 8601 formats. No need to invent formats such as seen in your input.

String manipulation rather than custom formatter

If all your inputs have the same ending of +0000Z, I suggest you clean the incoming data rather than define a formatting pattern.

String input = "2022-03-12T03:59:59+0000Z".replace( "+0000Z" , "Z" ) ;
Instant instant = Instant.parse( input ) ;

Other formats

You later commented:

"2017-01-04T12:30:00-05:00" , "2017-03-20T22:05:00Z", etc. So I cannot assume all my inputs to have the same ending of +0000Z.

Both of those are standard ISO 8601 formats. Both can be parsed as OffsetDateTime objects as-is, without alteration.

So I still maintain that the simplest approach, given your range of possible inputs, is to do the replace string manipulation first, which has no effect if your other two formats arrive. Then parse all three variations as OffsetDateTime objects. Sometimes programmers tend to over-think a problem, and over-engineer an elaborate solution where a simple one suffices.

Example code:

OffsetDateTime odt1 = OffsetDateTime.parse( "2022-03-12T03:59:59+0000Z".replace( "+0000Z" , "Z" ) ) ;
OffsetDateTime odt2 = OffsetDateTime.parse( "2017-01-04T12:30:00-05:00".replace( "+0000Z" , "Z" ) ) ;
OffsetDateTime odt3 = OffsetDateTime.parse( "2017-03-20T22:05:00Z".replace( "+0000Z" , "Z" ) ) ;

2022-03-12T03:59:59Z

2017-01-04T12:30-05:00

2017-03-20T22:05Z

See this code run live at IdeOne.com.

Trap for DateTimeParseException while parsing to detect yet another unexpected format.

Of course, the best solution is to educate the publisher of your data about consistent use of ISO 8601 formats such as the latter two examples, while avoiding the screwy first example’s format.

Count since epoch

I recommend against tracking time as a count-since-epoch. Such a count is inherently ambiguous and error-prone. Instead, use text in standard ISO 8601 format. But if you insist on a count, here it is.

To get a count of seconds since the epoch reference of 1970-01-01T00:00Z, extract a Instant from the OffsetDateTime, and interrogate.

long secondsSinceEpoch = odt.toInstant().getEpochSecond() ;
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • For my part I’d prefer the custom formatter (reusing `ISO_LOCAL_DATE_TIME` as part of it) over string manipulation. I upvoted for the good information about `ZonedDateTime` being inappropriate and redundancy. – Ole V.V. Mar 15 '22 at 08:51
  • Thank you Basil. My input can have different format as well. For example, "2017-01-04T12:30:00-05:00" , "2017-03-20T22:05:00Z", etc. So I cannot assume all my inputs to have the same ending of +0000Z. I cannot predict. So I think date formatter with several optional parts fits better for my usecase. – Zack Mar 15 '22 at 19:14
  • @Zack See my edits at bottom of Answer. Really no need to build an elaborate custom formatter for your three mentioned formats. Just do `String#replace` on all incoming strings. If there is nothing to replace, you get back the original string, no problem. I would rather read (and debug!) a single `replace` call than a 15-calls-long method chain. But, to each his own. – Basil Bourque Mar 15 '22 at 20:14
  • Yes. I understand . I like the replace approach too but then I have to do some testing and make sure it works fine for several incoming formats. Best approach would be to have an agreement with the publisher (long term solution). I will try out the replace approach and update if I find anything that does not work. Thanks again Basil. – Zack Mar 15 '22 at 22:01
  • 1
    Actually my requirement is to get epoch second from the input date string. So I might just use Instant with replacement. String input = "2022-03-12T03:59:59+0000Z".replace( "+0000Z" , "Z" ) ; long epochSeconds = Instant.parse( input ).getEpochSecond() ; – Zack Mar 15 '22 at 22:11
  • 2
    @Zack You can call OffsetDateTime toInstant getEpochSecond – Basil Bourque Mar 15 '22 at 22:12
  • 1
    Is `DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss[XXX][XX][X]")` that hard to read or debug? As you said, each must decide for themself. It parses all the mentioned examples fine. Again one may reuse `DateTimeFormatter.ISO_LOCAL_DATE_TIME` for the non-optional part if desired (I would). – Ole V.V. Mar 16 '22 at 11:15
  • 1
    @OleV.V. Any Java programmer can recognize the meaning `.replace( "+0000Z" , "Z" )`. Very few could decipher `DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss[XXX][XX][X]")`. I am fairly comfortable with *java.time*, but even I would have to look up the meaning of single, double, and triple uppercase `X` and what they mean in succession in a formatter. I'm not saying the formatter-based solution is *wrong*; I am saying that *in this case* it is overkill with string manipulation being much simpler. In other more complicated cases, the formatter approach might well be more appropriate. – Basil Bourque Mar 17 '22 at 00:12