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?
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?
1 answer
The following users marked this post as Works for me:
User | Comment | Date |
---|---|---|
hkotsubo | (no comment) | Oct 5, 2021 at 01:34 |
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.
-
IMO, it doesn't make any sense :-) ↩︎
0 comment threads