Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

How to parse a date with more than 3 decimal digits in the fractions of second?

+6
−0

I'm using SimpleDateFormat to parse a string containing a date/time, but the result has a different date, hour, minute, second and millisecond:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS");
Date date = sdf.parse("2021-10-01T10:30:45.123456789");
System.out.println(date); // Sat Oct 02 20:48:21 BRT 2021

// get the millisecond value
Calendar cal = Calendar.getInstance();
cal.setTime(date);
System.out.println(cal.get(Calendar.MILLISECOND)); // 789

As you can see, the input date is October 1st 2021, at 10:30:45 AM, but the output date is October 2nd 2021, at 8:48:21 PM. As printing a Date uses its toString() method, and it doesn't show the milliseconds, I've used a Calendar to get this value: I expected it to be 123, but it was 789.

I noticed that it happens only when the fraction of seconds has more than 3 decimal digits (with 3 or less digits, it works). But I need to parse dates with up to 9 decimal digits, so how can I do it? And why is SimpleDateFormat changing the date/time values?

History
Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

1 answer

+6
−0

The solution depends on the Java version you're using. First, let's see the solution for earlier versions, that doesn't use SimpleDateFormat. Then we'll see why the problem happens and alternatives for older versions.


JDK >= 8

For JDK >= 8, you can (should/must?) use the java.time API. It supplants the old legacy API (Date, Calendar, SimpleDateFormat) and should be preferred for newer projects (unless you're stuck with the legacy for any reason).

The old API has milissecond precision and can't correctly handle more than 3 decimal digits. The new API has nanosecond precision and can handle up to 9 decimal digits without any problem.

Another difference is that in java.time there are lots of different types to represent dates and times. In your case, the input string has date and time, so the best fit is probably a java.time.LocalDateTime:

LocalDateTime dt = LocalDateTime.parse("2021-10-01T10:30:45.123456789");
System.out.println(dt); // 2021-10-01T10:30:45.123456789

As the input string is in ISO 8601 format, it can be parsed directly. But you can also use a java.time.format.DateTimeFormatter:

DateTimeFormatter parser = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS");
LocalDateTime dt = LocalDateTime.parse("2021-10-01T10:30:45.123456789", parser);

The difference is that this DateTimeFormatter accepts exactly 9 decimal digits, while the one-arg parse method above uses a built-in parser that accepts from 0 to 9 digits:

// OK, it accepts from 0 to 9 decimal digits
LocalDateTime dt = LocalDateTime.parse("2021-10-01T10:30:45.12345");

// error, it accepts exactly 9 decimal digits
DateTimeFormatter parser = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS");
dt = LocalDateTime.parse("2021-10-01T10:30:45.12345", parser); // DateTimeParseException

Anyway, if the input string is in ISO 8601 format, there's no need to build a custom DateTimeFormatter (unless you need to be strict about the exact number of decimal digits).

But what if I need a different format (let's say, "day/month/year"), and the flexibility of a variable number of decimal digits? In that case, you can use a java.time.format.DateTimeFormatterBuilder:

DateTimeFormatter parser = new DateTimeFormatterBuilder()
    // let's say I need a different date format
    .appendPattern("dd/MM/uuuu HH:mm:ss")
    // fraction of second, from 0 to 9 digits and the decimal separator
    .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
    .toFormatter();
LocalDateTime dt = LocalDateTime.parse("01/10/2021 10:30:45.12345", parser);

// output uses toString(), which always shows in ISO 8601
System.out.println(dt); // 2021-10-01T10:30:45.123450
// using the same DateTimeFormatter, it uses the specific format
System.out.println(dt.format(parser)); // 01/10/2021 10:30:45.12345

Showing all the parsing/formatting options is beyond the scope of the question, so please refer to the documentation to see all the possibilities. The point here is: if you need to handle up to 9 decimal digits in the fraction of seconds, java.time is the best choice, IMO.

Depending on the input, different types might be needed, as the API provides lots of options (such as LocalTime for time-only data, ZonedDateTime if you need to deal with timezones, etc). All of them can handle up to 9 decimal digits. And if you "need" a Date or Calendar instance (maybe because you're using legacy code that works only with those classes), there are ways to convert to/from them.


JDK <= 7

If you're using JDK <= 7 (or is using a newer Java version, but has legacy code that still uses the old API), you can't use SimpleDateFormat directly like you did, because it'll return incorrect dates.

As previously said, the old date/time API (Date, Calendar, SimpleDateFormat) has millisecond precision and can't handle more than 3 decimal digits in the fraction of seconds.

But SimpleDateFormat didn't give any errors...

Yes, SimpleDateFormat is known to be very lenient and accept almost everything as input, and "tries its best" to give you a Date, even if it leads to wrong results (you can see some examples here and here).

In this specific case, the S token used in SimpleDateFormat pattern means "milliseconds" (while in java.time API, it means "fraction of seconds" - with nanosecond precision: same letter, different meaning, that's why it works in one API while failing in the other).

Therefore, when SSSSSSSSS is used with SimpleDateFormat, it means that 9 digits will be parsed and set to the milliseconds value. So in the string 2021-10-01T10:30:45.123456789, the parser reads the last 9 digits as "123456789 milliseconds", which in turn is equivalent to "34 hours, 17 minutes, 36 seconds and 789 milliseconds". And SimpleDateFormat adds this to the date/time values previously read. If that makes sense or not, it's a discussion for another day[1]. But that's what happened, and it explains why the resulting Date has some values that are completely different from the input.

If you need to store all the decimal digits, you'll have to store them separately from the date. The best you can do is to store the whole fraction of seconds in another field/variable, and optionally keep the Date with the milliseconds (the first 3 decimal digits). Something like this:

String s = "2021-10-01T10:30:45.12345";
// parse the date/time without the decimal digits
String[] parts = s.split("\\.");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
Date date = sdf.parse(parts[0]);

// check if there are decimal digits, store the value as nanoseconds
int nanos;
if (parts.length == 1) { // no decimal digits
    nanos = 0;
} else {
    // right-pad with zeroes, so the value is in nanoseconds
    nanos = Integer.parseInt(String.format("%-9s", parts[1]).replaceAll(" ", "0"));
    // The Date has no milliseconds, but we can add it
    date.setTime(date.getTime() + (nanos / 1000000));
}
System.out.println(sdf.format(date)); // 2021-10-01T10:30:45
System.out.println(nanos); // 123450000

You could also validate if parts[1].length is greater than 9 (and show an error message in that case, if the maximum precision you want to handle is 9 digits, of course), etc. But the general ideia is to store the fraction of seconds in a separate field, in case you don't want to lose precision.

If you don't mind losing precision and want to preserve just the first 3 decimal digits, you can use date.setTime(date.getTime() + (nanos / 1000000)) as shown above, and then discard the nanos variable. This is needed because we used a pattern without milliseconds, so SimpleDateFormat sets its value to zero.

Alternative for JDK 6 and 7

If you're using JDK 6 or 7, it's possible to use the Three-Ten Backport, which provides a backport of the java.time API.

It's very similar to java.time, the difference is that all classes are in the org.threeten.bp package (the names of the classes and their methods are the same, though, and all have nanosecond precision). One exception is the conversion to/from Date/Calendar: in Java >= 8 it's implemented as methods of those classes, but in the backport this is made by an utilitary class.

With the backport, the code is basically the same as above, with DateTimeFormatter, LocalDateTime, etc.


  1. IMO, it doesn't make any sense :-) ↩︎

History
Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »