简体   繁体   中英

Java 8: How to create DateTimeFormatter with milli, micro or nano seconds?

I need to create the formatter for parsing timestamps with optional milli, micro or nano fractions of second.

For example, for my needs I see the following opportunity:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                                   .append(DateTimeFormatter.BASIC_ISO_DATE)
                                   .appendLiteral('-')
                                   .append(DateTimeFormatter.ISO_LOCAL_TIME)
                                   .appendOffset("+HH:mm", "Z")
                                   .toFormatter();

Or it is also possible to use appendFraction(field, minWidth, maxWidth, decimalPoint) .

However in these cases will it be possible to parse timestamps with any number of decimals (up to 9 or maxWidth). How to achieve that we can parse (optionally) only 3, 6 or 9 numbers after the comma?

It should be possible to parse the following time parts:

  • HH:mm:ss.SSS
  • HH:mm:ss.SSSSSS
  • HH:mm:ss.SSSSSSSSS

But impossible to parse: HH:mm:ss.SSSS .

You can use the optional sections pattern (delimited by [] ), and create 3 optional sections: 1 for 9 digits, another for 6 digits and another one for 3 digits.

According to DateTimeFormatterBuilder docs , you can use the S pattern (which is equivalent to NANO_OF_SECOND field ):

Pattern  Count  Equivalent builder methods
-------  -----  --------------------------
S..S     1..n   appendFraction(ChronoField.NANO_OF_SECOND, n, n, false)

In the old API ( SimpleDateFormat ), S is the pattern used for milliseconds , but in the new API it was changed to nanoseconds.

So the formatter will be created like this:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    // here is the same as your code
    .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-')
    // time (hour/minute/seconds)
    .appendPattern("HH:mm:ss")
    // optional nanos, with 9, 6 or 3 digits
    .appendPattern("[.SSSSSSSSS][.SSSSSS][.SSS]")
    // offset
    .appendOffset("+HH:mm", "Z")
    // create formatter
    .toFormatter();

Some tests:

// 3 digits
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123Z", formatter)); // 2016-12-01T10:30:45.123Z

// 6 digits
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123456Z", formatter)); // 2016-12-01T10:30:45.123456Z

// 9 digits
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123456789Z", formatter)); // 2016-12-01T10:30:45.123456789Z

// 4 digits (throws DateTimeParseException: Text '20161201-10:30:45.1234Z' could not be parsed at index 21)
System.out.println(OffsetDateTime.parse("20161201-10:30:45.1234Z", formatter));

The output is:

2016-12-01T10:30:45.123Z
2016-12-01T10:30:45.123456Z
2016-12-01T10:30:45.123456789Z
Exception in thread "main" java.time.format.DateTimeParseException: Text '20161201-10:30:45.1234Z' could not be parsed at index 21


Notes:

  • This instance of DateTimeFormatter is not good for formatting , because it prints all optional sections (so the nanosecond will be printed 3 times):

     // don't use it to format, it prints all optional sections // (so nanos are printed 3 times: with 9, 6 and 3 digits) OffsetDateTime odt = OffsetDateTime.parse("20161201-10:30:45.123Z", formatter); System.out.println(formatter.format(odt)); // output is 20161201Z-10:30:45.123000000.123000.123Z 

So if you want to display your dates in another format, consider creating another DateTimeFormatter .

  • In your code you used DateTimeFormatter.ISO_LOCAL_TIME . According to javadoc , in this formatter the seconds are optional. If you want to have the same behaviour, just change the time pattern to:

     // time (hour/minute) with optional seconds .appendPattern("HH:mm[:ss]") 
  • The [] pattern is a nice shortcut to make optional sections, but you could also create them using optionalStart() with appendFraction() :

     DateTimeFormatter formatter = new DateTimeFormatterBuilder() // here is the same as your code .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-') // time (hour/minute/seconds) .appendPattern("HH:mm:ss") // optional nanos with 9 digits (including decimal point) .optionalStart() .appendFraction(ChronoField.NANO_OF_SECOND, 9, 9, true) .optionalEnd() // optional nanos with 6 digits (including decimal point) .optionalStart() .appendFraction(ChronoField.NANO_OF_SECOND, 6, 6, true) .optionalEnd() // optional nanos with 3 digits (including decimal point) .optionalStart() .appendFraction(ChronoField.NANO_OF_SECOND, 3, 3, true) .optionalEnd() // offset .appendOffset("+HH:mm", "Z") // create formatter .toFormatter(); 

This formatter works exactly the same way as the first one created with [] pattern.


Side effect

As @SeanVanGorder noticed in his comment , this formatter has the side effect of accepting multiple patterns for the nanosecond field, but it works only if the values are the same:

// side effect
// multiple nanos values (accepts multiple values if they're all the same)
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123000.123Z", formatter)); // 2016-12-01T10:30:45.123Z
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123000000.123Z", formatter)); // 2016-12-01T10:30:45.123Z
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123000000.123000.123Z", formatter)); // 2016-12-01T10:30:45.123Z

All the lines above output 2016-12-01T10:30:45.123Z , but notice that they accept all the optional values (like .123000000.123 ). As the values are the same, the parsing is done without errors.

If the values are different, though, it throws an exception:

// multiple nanos values (throws exception if values are different)
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123000.124Z", formatter)); // exception

If this behaviour is not desired, the only alternative is to create lots of different formatters (one for each case) and do a for loop until you get a valid parsed value (very similar to this answer ).

First I created a method receives a list of DateTimeFormatter 's and a TemporalQuery to convert the parsed string to any object you want:

// method to parse, it receives a list of DateTimeFormatter and a TemporalQuery to convert the parsed string
public <T> T parse(String input, TemporalQuery<T> query, DateTimeFormatter... formatters) {
    for (DateTimeFormatter fmt : formatters) {
        try {
            // try to parse
            return fmt.parse(input, query);
        } catch (Exception e) {}
    }

    // none worked, throw exception
    throw new DateTimeParseException("Text '" + input + "' could not be parsed", input, 0);
}

Now you just need to create the formatters and use them in the parse method:

// alternative: have 3 different formatters
DateTimeFormatter f1 = new DateTimeFormatterBuilder()
    // here is the same as your code
    .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-')
    // time (hour/minute/seconds/3 digit nanos)
    .appendPattern("HH:mm:ss.SSS")
    // offset
    .appendOffset("+HH:mm", "Z")
    // create formatter
    .toFormatter();
DateTimeFormatter f2 = new DateTimeFormatterBuilder()
    // here is the same as your code
    .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-')
    // time (hour/minute/seconds/6 digit nanos)
    .appendPattern("HH:mm:ss.SSSSSS")
    // offset
    .appendOffset("+HH:mm", "Z")
    // create formatter
    .toFormatter();
DateTimeFormatter f3 = new DateTimeFormatterBuilder()
    // here is the same as your code
    .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-')
    // time (hour/minute/seconds/9 digit nanos)
    .appendPattern("HH:mm:ss.SSSSSSSSS")
    // offset
    .appendOffset("+HH:mm", "Z")
    // create formatter
    .toFormatter();

// all formatters
DateTimeFormatter[] formatters = new DateTimeFormatter[] { f1, f2, f3 };

// 3 digits
System.out.println(parse("20161201-10:30:45.123Z", OffsetDateTime::from, formatters)); // 2016-12-01T10:30:45.123Z
// 6 digits
System.out.println(parse("20161201-10:30:45.123456Z", OffsetDateTime::from, formatters)); // 2016-12-01T10:30:45.123456Z
// 9 digits
System.out.println(parse("20161201-10:30:45.123456789Z", OffsetDateTime::from, formatters)); // 2016-12-01T10:30:45.123456789Z

// 4 digits (throws exception)
System.out.println(parse("20161201-10:30:45.1234Z", OffsetDateTime::from, formatters));
// java.time.format.DateTimeParseException: Text '20161201-10:30:45.1234Z' could not be parsed

// multiple values (throws exception)
System.out.println(parse("20161201-10:30:45.123000.123Z", OffsetDateTime::from, formatters));
// java.time.format.DateTimeParseException: Text '20161201-10:30:45.123000.123Z' could not be parsed

Note that I used the method reference OffsetDateTime::from as a TemporalQuery , but you can change it to any query you need.

DateTimeFormatter only supports width ranges, so this wouldn't be possible with a single instance. You could make 3 separate formatters using .appendFraction(NANO_OF_SECOND, #, #, true) where # is 3, 6, or 9. Then try them in sequence, ignoring any DateTimeParseException until the last one:

private static TemporalAccessor parse(String text) {
    try {
        return formatter3.parse(text);
    } catch (DateTimeParseException e) {
        // ignore
    }
    try {
        return formatter6.parse(text);
    } catch (DateTimeParseException e) {
        // ignore
    }
    return formatter9.parse(text); // let this one throw
}

Another option would be checking the input with a regular expression first, something like text.matches("[^.]+(.+\\\\.(\\\\d{3}|\\\\d{6}|\\\\d{9})\\\\b.*)?") .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM