iso-8601 parser in Java/Spring with comma separator

Viewed 546

According to http://apiux.com/2013/03/20/5-laws-api-dates-and-times/ we should use ISO 8601 to format date. The output on my system is:

$ date --iso-8601=ns
2020-10-29T10:38:59,112768965+01:00

Which java formatter parse this string correctly? I've tried DateTimeFormatter.ISO_OFFSET_DATE_TIME and a few others, and they don't like this format. I expect that such a format should be supported out-of-the-box, as Oracle claims on its page https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html but it isn't.

Another thing is that Spring announced wide support for date format: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.3-Release-Notes#date-time-conversion-in-web-applications. I can then set all properties to "iso". But which exactly format is it, and where is it in use?

I see two problems here.

I would like to have consistent approach to date format across whole application, and I would like to use standard to easily integrate with the rest of the world. What should I use?

Edit: In the end it happened that there are a few things to consider:

  • Example from Linux console was artificial, as I didn't notice this format with "," in real world. It turned out, that it's slightly inconsistently supported in Java world
  • ObjectMapper, ModelMapper and OffsetDateTime makes Date object conversion different, especially conversion to ISO format ('Z' or '00:00')
  • Offset definition in form of "0000" also existed in Java, and I had to fix my conversion
3 Answers

ISO 8601-1 allows both , and . as decimal separators, though in practice this is the first time I've seen , actually in use. The default formats in JDK 11 only support parsing . (even though the documentation says the decimal separator is localised). To handle this format you have some options:

  1. Modify the string before parsing:
OffsetDateTime.parse(value.replaceAll(",", "."));
  1. Define a custom format:
 new DateTimeFormatterBuilder()
         .parseCaseInsensitive()
         .append(ISO_LOCAL_DATE)
         .appendLiteral('T')
         .appendValue(HOUR_OF_DAY, 2)
         .appendLiteral(':')
         .appendValue(MINUTE_OF_HOUR, 2)
         .optionalStart()
         .appendLiteral(':')
         .appendValue(SECOND_OF_MINUTE, 2)
         .optionalStart()
         .appendLiteral(',')
         .appendFraction(NANO_OF_SECOND, 0, 9, false)
         .parseLenient()
         .appendOffsetId()
         .toFormatter()
         .parse(value)
  1. Use a locale that uses , separators (confirmed working in JDK 15):
DateTimeFormatter.ISO_OFFSET_DATE_TIME.localizedBy(Locale.FRANCE).parse(value);

There does not seem to be a way to define a format that allows either , or . but not both. You'd have to either try one catch the exception and try the other, or detect which format it's in (e.g. value.contains(",")) and use the appropriate formatter.

If your string always has got 9 decimals after the comma, just specify a comma in the format pattern string:

    DateTimeFormatter isoWithCommaFormatter = DateTimeFormatter
            .ofPattern("uuuu-MM-dd'T'HH:mm:ss,SSSSSSSSSXXX");
    String iso = "2020-10-29T10:38:59,112768965+01:00";
    OffsetDateTime odt = OffsetDateTime.parse(iso, isoWithCommaFormatter);
    System.out.println(odt);

Output:

2020-10-29T10:38:59.112768965+01:00

If the number of decimals may vary, you need a DateTimeFormatterBuilder, but the basic trick is the same simple one: put a comma in the format pattern string.

    DateTimeFormatter isoWithCommaFormatter = new DateTimeFormatterBuilder()
            .append(DateTimeFormatter.ISO_LOCAL_DATE)
            .appendLiteral('T')
            .appendPattern("HH:mm:ss[,")
            .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, false)
            .appendPattern("]XXX")
            .toFormatter();

The ISO 8601 format recommends comma over dot as decimal separator, so it’s a little bit funny that the built in formatters and the one-arg parse methods only accept dot.

You can make the . or , as an optional pattern by putting them in a square bracket.

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        DateTimeFormatter isoFormatter = new DateTimeFormatterBuilder()
                .appendPattern("u-M-d'T'H:m:s[.][,]")
                .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, false)
                .appendPattern("[XXX][XX][X]")
                .toFormatter(Locale.ENGLISH);

        // Test
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30,123456789+02:00", isoFormatter));
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30,123456789+0200", isoFormatter));
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30,123456789+02", isoFormatter));
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30.123456789+02:00", isoFormatter));
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30+02:00", isoFormatter));
    }
}

Output:

2021-05-21T10:20:30.123456789+02:00
2021-05-21T10:20:30.123456789+02:00
2021-05-21T10:20:30.123456789+02:00
2021-05-21T10:20:30.123456789+02:00
2021-05-21T10:20:30+02:00

Alternatively,

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        DateTimeFormatter isoFormatter = DateTimeFormatter.ofPattern(
                "u[-M[-d]]['T'H[:m[:s[.][,][SSSSSSSSS][SSSSSSSS][SSSSSSS][SSSSSS][SSSSS][SSSS][SSS][SS][S]]]][XXX][XX][X]",
                Locale.ENGLISH);

        // Test
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30,123456789+02:00", isoFormatter));
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30,123456789+0200", isoFormatter));
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30,123456789+02", isoFormatter));
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30.123456789+02:00", isoFormatter));
        System.out.println(OffsetDateTime.parse("2021-05-21T10:20:30+02:00", isoFormatter));
    }
}

The output is the same.

Related