Search code examples
javadatesimpledateformat

SimpleDateFormat showing incorrect milliseconds with "S" format, but not with "SSS"


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.


Solution

  • 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: .0
    • SS: .05
    • SSS: .050
    • SSSS: .0500 and so on up to
    • SSSSSSSSS: .050000000

    This 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.