Converting ISO 8601-compliant String to ZonedDateTime
Asked Answered
V

1

0

I'm parsing logs from different sources, I'm extracting datetime string from log. Now I want to convert it into java ZonedDateTime.

Problem here is, I don't know exact datetime format, I just know that, string will be ISO-8601 compliant. So I want to write function like :

    /**
     * @param _dateTime : any ISO 8601 format , e.g 1. %Y-%m-%dT%H:%M:%s%z => 2014-05-25T08:20:03.123456Z , 
     *                    e.g 2. %Y-%m-%dT%H:%M:%s => 2014-05-25T08:20:03.123456, e.g 3. %Y-%m-%d %H:%M:%s%z => 2014-11-08 15:55:55.123456Z, 
     *                    e.g 4. %Y-%m-%d %H:%M:%s => 2014-11-08 15:55:55
     * @return Instance of ZonedDateTime if conversion successful
     * @throws DateTimeParseException
     */
    private ZonedDateTime parse (String _dateTime) throws DateTimeParseException  {
        // Magic here.
    }

What is best way to do it in java ?

Vigesimal answered 31/3, 2021 at 12:6 Comment(5)
What do you mean you don't know the format? ISO 8601 datetime is a format.Koeninger
Your javadoc seems to suggest that using a space between the date and the time is an acceptable substitute for the letter T. That is not ISO 8601. ISO 8601 always uses T as the delimiter. "Separating date and time parts with other characters such as space is not allowed in ISO 8601"Koeninger
Like i mentioned in method description it could be like 2014-05-25T08:20:03.123456 or 2014-11-08 15:55:55.123456Z or 2014-11-08 15:55:55 etc ...Vigesimal
Your 4th example, 2014-11-08 15:55:55, is not ISO 8601 compliant. ISO 8601 requires a T to denote the start of the time part as in your 1st and 2nd example. Same problem with the 3rd example.Tight
When the string ends with a Z for UTC, clearly you should get a ZonedDateTime in UTC. When it doesn’t, which time zone do you want? It requires a time zone to construct a ZonedDateTime.Tight
T
4

That’s non-trivial alright. Most of the magic is in the following formatter:

private static final DateTimeFormatter formatter
        = new DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE)
                .appendPattern("['T'][' ']")
                .append(DateTimeFormatter.ISO_LOCAL_TIME)
                .appendPattern("[XX]")
                .toFormatter();

This formatter accepts

  • A date like 2014-05-25.
  • Either a T or a space (or both or neither, but I count on exactly one of them being in your string). The square brackets denote optional parts of the format. The single quotes denote literal parts and cause the T not to be interpreted as a pattern letter.
  • A time of day. ISO_LOCAL_TIME accepts seconds with and without a decimal fraction.
  • An optional UTC offset like Z (for zero) or -0600.

We also need a time zone for the cases where we haven’t got an offset in the string. For example:

private static final ZoneId defaultZone = ZoneId.of("America/Curacao");

What your method still needs to do is distinguish between the cases with and without UTC offset. The form with offset can be parsed directly into a ZonedDateTime. The one without cannot, so for it we need to parse into a LocalDateTime and convert.

/**
 * @param dateTime : any ISO 8601 format , e.g 1. %Y-%m-%dT%H:%M:%s%z => 2014-05-25T08:20:03.123456Z , 
 *                    e.g 2. %Y-%m-%dT%H:%M:%s => 2014-05-25T08:20:03.123456, 
 *                    e.g 3. %Y-%m-%d %H:%M:%s%z => 2014-11-08 15:55:55.123456Z, 
 *                    e.g 4. %Y-%m-%d %H:%M:%s => 2014-11-08 15:55:55
 * @return Instance of ZonedDateTime if conversion successful
 * @throws DateTimeParseException
 */
private static ZonedDateTime parse(String dateTime) {
    // Little magic here.
    TemporalAccessor parsed = formatter.parseBest(dateTime,
            ZonedDateTime::from, LocalDateTime::from);
    if (parsed instanceof ZonedDateTime) {
        return (ZonedDateTime) parsed;
    } else {
        return ((LocalDateTime) parsed).atZone(defaultZone);
    }
}

Let’s try it out. I have used your four examples plus one that I added between the 1st and the 2nd.

    System.out.println(parse("2014-05-25T08:20:03.123456Z"));
    System.out.println(parse("2014-05-25T10:20:03.123456+0200"));
    System.out.println(parse("2014-05-25T08:20:03.123456"));
    System.out.println(parse("2014-11-08 15:55:55.123456Z"));
    System.out.println(parse("2014-11-08 15:55:55"));

Output:

2014-05-25T08:20:03.123456Z
2014-05-25T10:20:03.123456+02:00
2014-05-25T08:20:03.123456-04:00[America/Curacao]
2014-11-08T15:55:55.123456Z
2014-11-08T15:55:55-04:00[America/Curacao]

As an aside prefer OffsetDateTime over ZonedDateTime for your strings with a UTC offset. The code will be the same except you will need a further conversion .toOffsetDateTime() after atZone(defaultZone) in the case where you don’t get an OffsetDateTime directly from parsing. A ZonedDateTime is for a date and time with a time zone like Europe/Prague. An OffsetDateTime is for a date and time with a UTC offset. As I mentioned, Z is an offset of 0 from UTC.

Edit: Arvind Kumar Avinash’ notes in a comment are useful and important enough to deserve to be in the answer proper: Some notes for the future visitors:

  1. Replacing [XX] with [XX][XXX] will also cater to the offset like +02:00.
  2. Date-time parsing/formatting types are Locale-sensitive and therefore, it's always advisable to use it with the applicable locale e.g. .toFormatter(Locale.ENGLISH) instead of .toFormatter().
  3. You can use ZoneId.systemDefault() if you want the ZoneId of the JVM to be picked up automatically.
Tight answered 1/4, 2021 at 16:10 Comment(1)
Some notes for the future visitors: (1) Replacing [XX] with [XX][XXX] will also cater to the offset like +02:00 (2) Date-time parsing/formatting types are Locale-sensitive and therefore, it's always advisable to use it with the applicable locale e.g. .toFormatter(Locale.ENGLISH) instead of .toFormatter() (3) You can use ZoneId.systemDefault() if you want the ZoneId of the JVM to be picked up automatically.Douglas

© 2022 - 2024 — McMap. All rights reserved.