Is java.time failing to parse fraction-of-second?
Asked Answered
S

3

42

With the first release of Java 8 (b132) on Mac OS X (Mavericks), this code using the new java.time package works:

String input = "20111203123456"; 
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyyMMddHHmmss");
LocalDateTime localDateTime = LocalDateTime.parse( input, formatter );

Rendering:

2011-12-03T12:34:56

But when I add "SS" for fraction-of-second (and "55" as input), as specified in the DateTimeFormatter class doc, an exception is thrown:

java.time.format.DateTimeParseException: Text '2011120312345655' could not be parsed at index 0

The doc says Strict mode is used by default and requires the same number of format characters as input digits. So I'm confused why this code fails:

String input = "2011120312345655"; 
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyyMMddHHmmssSS");
LocalDateTime localDateTime = LocalDateTime.parse( input, formatter );

Another example using example from documentation ("978") (fails):

String input = "20111203123456978"; 
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyyMMddHHmmssSSS");
LocalDateTime localDateTime = LocalDateTime.parse( input, formatter );

This example works, adding a decimal point (but I find no such requirement in the doc):

String input = "20111203123456.978"; 
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyyMMddHHmmss.SSS");
LocalDateTime localDateTime = LocalDateTime.parse( input, formatter );

Renders:

localDateTime: 2011-12-03T12:34:56.978

Omitting the period character from either the input string or the format cause a fail.

Fails:

String input = "20111203123456.978"; 
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyyMMddHHmmssSSS");
LocalDateTime localDateTime = LocalDateTime.parse( input, formatter );

Fails:

String input = "20111203123456978"; 
DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyyMMddHHmmss.SSS");
LocalDateTime localDateTime = LocalDateTime.parse( input, formatter );
Sarina answered 23/3, 2014 at 7:8 Comment(13)
That fraction is nano second. Can you try with 9 digits? Although from reading the docs, it should have worked with less than 9 digits as long as the pattern match the number of digits.Oscillation
After re-reading the docs, can you try by having a decimal point after the seconds field? Like 20111203123456.978?Oscillation
Adding a decimal point makes it work. See addition at bottom of the Question. But can you point me to such a requirement in the doc? I'm trying to determine if I should be filing a bug report.Sarina
Firstly, good that it worked for you. Secondly, I hadn't seen it specifically written but more like my interpretation of fraction of a second. A fraction of second should be preceded by a decimal. It does not make sense not having decimal. To make a comparison with time HH:mm:ss, colon is normally used as a separator but it is not part of the field. Hence colon is not required because it is not part of a field. But fraction of a second is part of seconds field. So it should be separated by a decimal. That's how I intepret it. But feel free to log a bug report if you still think there's a bug.Oscillation
Why would the fraction-of-second deserve punctuation (a period) any more than minute-of-hour deserves a colon? I'm not being facetious.Sarina
Like I said, because fraction-of-second is PART OF a seconds field. It is not merely a separator like a colon separates Hours, minutes and seconds. However you can also argue since it is a fraction, a decimal point is presumed. On the other hand, the designers may also think that a decimal is necessary when specifying a fraction. What you could try to see if this still works. "20111203123456 .978" and apply "yyyyMMddHHmmss SSS"?Oscillation
@Oscillation (a) I understand your intuitive logic about fraction-of-second belonging to the second-of-minute. But the doc clearly holds fraction-of-second out as a separate entity like all the other parts. So I think I'll proceed with bug report. (b) I tried your last suggestion, omitting the decimal point from either the input or the formatter. Both cause a fail.Sarina
I think the tag "java-time" is maybe not so clear. Under this tag we also find entries which have nothing to do with the new date-and-time-API in Java 8. Here on SO the tag "jsr310" is far more common until now although I admit that the name is rather speaking to experts.Plasticity
@MenoHochschild thanks for the tip. I will add the jsr310 tag, and look into creating a new tag.Sarina
I added a new tag "java.time" with a pending wiki excerpt.Sarina
@BasilBourque, please contribute that wiki to the existing tag instead of creating a new one.Calabar
@Calabar Given what Meno said about "java-time" being used historically for other purposes pre-dating the JSR 310 API, it seems appropriate to create a new tag specifically targeting the new API and named the same as the new API. The old one has only been used 7 times. Why not delete/ignore the old and create a new one with a name that matches the API's package name? I read 3 entries on the Meta StackExchange and found no guidance to the contrary. In general I understand your concern to not pollute the tag pool, but this situation with "java.time" JSR 310 seems unique.Sarina
@BasilBourque, one of the big problems is that the SE tag system doesn't consider . in the same way it considers other punctuation. People looking for java-time aren't going to see java.time, and there's a horrible mix of tags in various places that use one instead of the other. If you think that the tag with the period will be more helpful, then let's completely obliterate the other tag to remove the ambiguity.Calabar
P
45

Bug – Fixed in Java 9

This issue was already reported in JDK-bug-log. Stephen Colebourne mentions as work-around following solution:

DateTimeFormatter dtf = 
  new DateTimeFormatterBuilder()
  .appendPattern("yyyyMMddHHmmss")
  .appendValue(ChronoField.MILLI_OF_SECOND, 3)
  .toFormatter();

Note: This workaround does not cover your use-case of only two pattern symbols SS. An adjustment might only be to use other fields like MICRO_OF_SECOND (6 times SSSSSS) or NANO_OF_SECOND (9 times SSSSSSSSS). For two fraction digits see my update below.

@PeterLawrey About the meaning of pattern symbol "S" see this documentation:

Fraction: Outputs the nano-of-second field as a fraction-of-second. The nano-of-second value has nine digits, thus the count of pattern letters is from 1 to 9. If it is less than 9, then the nano-of-second value is truncated, with only the most significant digits being output. When parsing in strict mode, the number of parsed digits must match the count of pattern letters. When parsing in lenient mode, the number of parsed digits must be at least the count of pattern letters, up to 9 digits.

So we see that S stands for any fraction of second (including nanosecond), not just milliseconds. Furthermore, the fractional part does at the moment not take well in adjacent value parsing, unfortunately.

EDIT:

As background here some remarks about adjacent value parsing. As long as fields are separated by literals like a decimal point or time part separators (colon), the interpretation of fields in a text to be parsed is not difficult because the parser then knows easily when to stop i.e. when the field part is ended and when the next field starts. Therefore the JSR-310 parser can process the text sequence if you specify a decimal point.

But if you have a sequence of adjacent digits spanning over multiple fields then some implementation difficulties arise. In order to let the parser know when a field stops in text it is necessary to instruct the parser in advance that a given field is represented by a fixed-width of digit chars. This works with all appendValue(...)-methods which assume numerical representations.

Unfortunately JSR-310 has not managed well to do this also with the fractional part (appendFraction(...)). If you look for the keyword "adjacent" in the javadoc of class DateTimeFormatterBuilder then you find that this feature is ONLY realized by appendValue(...)-methods. Note that the spec for pattern letter S is slightly different but internally delegates to appendFraction()-method. I assume we will at least have to waint until Java 9 (as reported in JDK-bug-log, or later???) until fraction parts can manage adjacent value parsing as well.


Update from 2015-11-25:

The following code using two fraction digits only does not work and misinterpretes the millisecond part:

    DateTimeFormatter dtf =
        new DateTimeFormatterBuilder()
            .appendPattern("yyyyMMddHHmmss")
            .appendValue(ChronoField.MILLI_OF_SECOND, 2)
            .toFormatter();
    String input = "2011120312345655";
    LocalDateTime ldt = LocalDateTime.parse(input, dtf);
    System.out.println(ldt); // 2011-12-03T12:34:56.055

The workaround

String input = "2011120312345655"; 
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSS");
Date d = sdf.parse(input);
System.out.println(d.toInstant()); // 2011-12-03T12:34:56.055Z

does not work because SimpleDateFormat interpretes the fraction in a wrong way, too, similar to the modern example (see output, 55 ms instead of 550 ms).

What is left as solution is either waiting an undertermined long time until Java 9 (or later?) or writing your own hack or using 3rd-party libraries as solution.

Solution based on a dirty hack:

String input = "2011120312345655"; 
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
int len = input.length();
LocalDateTime ldt = LocalDateTime.parse(input.substring(0, len - 2),  dtf);
int millis = Integer.parseInt(input.substring(len - 2)) * 10;
ldt = ldt.plus(millis, ChronoUnit.MILLIS);
System.out.println(ldt); // 2011-12-03T12:34:56.550

Solution using Joda-Time:

String input = "2011120312345655"; 
DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyyMMddHHmmssSS");
System.out.println(dtf.parseLocalDateTime(input)); // 2011-12-03T12:34:56.550

Solution using my library Time4J:

String input = "2011120312345655"; 
ChronoFormatter<PlainTimestamp> f = 
  ChronoFormatter.ofTimestampPattern("yyyyMMddHHmmssSS", PatternType.CLDR, Locale.ROOT);
System.out.println(f.parse(input)); // 2011-12-03T12:34:56.550

Update from 2016-04-29:

As people can see via the JDK-issue mentioned above, it is now marked as resolved - for Java 9.

Plasticity answered 24/3, 2014 at 10:28 Comment(2)
In your update from 2015-11-25, you say that this formatter throws a DateTimeParseException: new DateTimeFormatterBuilder().appendPattern("yyyyMMddHHmmssSS").appendValue(ChronoField.MILLI_OF_SECOND, 2). But it does not throw an exception if you remove the "SS" at the end of the pattern (which shouldn't be there since you use appendValue() for the milliseconds). Only problem is it interprets the fraction incorrectly, returning "55" instead of "550", like SimpleDateFormat.Philosopher
@EtienneNeveu Thank you very much. I have now corrected my statement from 2015 according to your comment.Plasticity
R
1
DateTimeFormatterBuilder#appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true)

Something like this helped me

Richmound answered 16/5, 2019 at 9:48 Comment(0)
L
0

Here's an algorithm which adjusts the order of the trailing zeros that are conventionally returned from the formatted date String.

/**
 * Takes a Date and provides the format whilst compensating for the mistaken representation of sub-second values.
 * i.e. 2017-04-03-22:46:19.000991 -> 2017-04-03-22:46:19.991000
 * @param pDate Defines the Date object to format.
 * @param pPrecision Defines number of valid subsecond characters contained in the system's response.
 * */
private static final String subFormat(final Date pDate, final SimpleDateFormat pSimpleDateFormat, final int pPrecision) throws ParseException {
    // Format as usual.
    final String lString        = pSimpleDateFormat.format(pDate);
    // Count the number of characters.
    final String lPattern       = pSimpleDateFormat.toLocalizedPattern();
    // Find where the SubSeconds are.
    final int    lStart         = lPattern.indexOf('S');
    final int    lEnd           = lPattern.lastIndexOf('S');
    // Ensure they're in the expected format.
    for(int i = lStart; i <= lEnd; i++) { if(lPattern.charAt(i) != 'S') {
        // Throw an Exception; the date has been provided in the wrong format.
       throw new ParseException("Unable to process subseconds in the provided form. (" + lPattern + ").", i);
    } }
    // Calculate the number of Subseconds. (Account for zero indexing.)
    final int lNumSubSeconds = (lEnd - lStart) + 1;
    // Fetch the original quantity.
    String lReplaceString = lString.substring(lStart + (lNumSubSeconds - pPrecision), lStart + lNumSubSeconds);
    // Append trailing zeros.
    for(int i = 0; i < lNumSubSeconds - pPrecision; i++) { lReplaceString += "0"; }
    // Return the String.
    return lString.substring(0, lStart) + lReplaceString;
}
Lineage answered 4/4, 2017 at 10:45 Comment(2)
Answers on Stack Overflow are better when they include some discussion or explanation.Sarina
@BasilBourque You're absolutely right. I was hoping the comments would speak for themselves, but you're correct that it at the very least deserves some context.Lineage

© 2022 - 2024 — McMap. All rights reserved.