I'm experiencing a problem where a displayed date has the millisecond component multiplied by 10.
Specifically, the time 52.050
is being shown as 52.50
when a .S
format is used, but 52.050
when a .SSS
format is used.
Take the following code example:
// Some arbitrary point with 50 milliseconds
final Date date = new Date(1620946852050 l);
final LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
final String format = "%-40s%-20s%-20s%n";
System.out.format(format, "Date Formatter", "Date Format", "Formatted Output");
Stream.of("HH:mm:ss", "HH:mm:ss.S", "HH:mm:ss.SS", "HH:mm:ss.SSS").forEach(dateFormat - > {
System.out.println();
System.out.format(format, SimpleDateFormat.class.getName(), dateFormat,
new SimpleDateFormat(dateFormat).format(date));
System.out.format(format, DateTimeFormatter.class.getName(), dateFormat,
DateTimeFormatter.ofPattern(dateFormat).format(localDateTime));
});
This produces an output of:
Date Formatter Date Format Formatted Output
java.text.SimpleDateFormat HH:mm:ss 00:00:52
java.time.format.DateTimeFormatter HH:mm:ss 00:00:52
java.text.SimpleDateFormat HH:mm:ss.S 00:00:52.50
java.time.format.DateTimeFormatter HH:mm:ss.S 00:00:52.0
java.text.SimpleDateFormat HH:mm:ss.SS 00:00:52.50
java.time.format.DateTimeFormatter HH:mm:ss.SS 00:00:52.05
java.text.SimpleDateFormat HH:mm:ss.SSS 00:00:52.050
java.time.format.DateTimeFormatter HH:mm:ss.SSS 00:00:52.050
I've used both java.util.Date
and java.time
to illustrate the unexpected behaviour, and I'm aware that java.time
is better, but I'd still like to understand the SimpleDateFormat
behaviour.
I'm running Java 14.0.2.12, but can reproduce in 11.0.10.9.
The legacy date-time API (java.util
date-time types and their formatting type, SimpleDateFormat
) is outdated and error-prone. It is recommended to stop using it completely and switch to java.time
, the modern date-time API*.
Let's understand how the result produced by SimpleDateFormat
is confusing (and hence error-prone).
1620946852050 milliseconds = 1620946852000 milliseconds + 50 milliseconds
With 1620946852000, System.out.println(localDateTime)
will produce the result, 2021-05-14T00:00:52
.
50 milliseconds = (50 / 1000) seconds = 0.05 seconds. This is how DateTimeFormatter
also presents it. The documentation describes S
clearly as: fraction-of-second. In other words, it presents it as a mathematical floating point number of 0.05 up to 9 places of decimal (nanosecond).
You can understand it this way: Pad zeros to the right side of the String.valueOf(0.05)
to turn the precision up to 9 places of decimal. Thus, it becomes "0.050000000"
. Now, based on the number of S
, get the substring of "0.050000000"
. Note that you can do so only up to 9 places i.e. SSSSSSSSSS
will throw an exception.
S
: .0SS
: .05SSS
: .050SSSS
: .0500 and so on up toSSSSSSSSS
: .050000000This is the representation we learned in our childhood when we learnt fraction in Mathematics.
On the other hand, SimpleDateFormat
does not present it as a fraction-of-second; rather, it presents the number after .
as the number of milliseconds. The documentation describes S
as: Millisecond. So, it presents it as a mathematical decimal integer number of 50. This makes it confusing because as soon as we see .
, we think of fraction whereas SimpleDateFormat
considers it just a separator for seconds and milliseconds.
The following example illustrates these different ways of presentation:
public class Main {
public static void main(String[] args) {
int sdf = 50;
String dtf = String.format("%.9f", 0.05);
System.out.format("sdf: %01d, dtf: %s%n", sdf, dtf.substring(0, 3));// Including two places for "0."
System.out.format("sdf: %02d, dtf: %s%n", sdf, dtf.substring(0, 4));// Including two places for "0."
System.out.format("sdf: %03d, dtf: %s%n", sdf, dtf.substring(0, 5));// Including two places for "0."
System.out.format("sdf: %04d, dtf: %s%n", sdf, dtf.substring(0, 6));// Including two places for "0."
}
}
Output:
sdf: 50, dtf: 0.0
sdf: 50, dtf: 0.05
sdf: 050, dtf: 0.050
sdf: 0050, dtf: 0.0500
* For any reason, if you have to stick to Java 6 or Java 7, you can use ThreeTen-Backport which backports most of the java.time functionality to Java 6 & 7. If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project. Learn more about the modern date-time API from Trail: Date Time.