简体   繁体   中英

How to format an elapsed time interval in hh:mm:ss.SSS format in Java?

I'm making a stop watch where I'm using Java's SimpleDateFormat to convert the number of milliseconds into a nice "hh:mm:ss:SSS" format. The problem is the hours field always has some random number in it. Here's the code I'm using:

public static String formatTime(long millis) {
    SimpleDateFormat sdf = new SimpleDateFormat("hh:mm:ss.SSS");

    String strDate = sdf.format(millis);
    return strDate;
}

If I take off the hh part then it works fine. Otherwise in the hh part it'll display something random like "07" even if the argument passed in (number of milliseconds) is zero.

I don't know much about the SimpleDateFormat class though. Thanks for any help.

Support for what you want to do is built in to the latest JDKs with a little known class called TimeUnit .

What you want to use is java.util.concurrent.TimeUnit to work with intervals .

SimpleDateFormat does just what it sounds like it does, it formats instances of java.util.Date , or in your case it converts the long value into the context of a java.util.Date and it doesn't know what to do with intervals which is what you apparently are working with.

You can easily do this without having to resort to external libraries like JodaTime.

import java.util.concurrent.TimeUnit;

public class Main
{        
    private static String formatInterval(final long l)
    {
        final long hr = TimeUnit.MILLISECONDS.toHours(l);
        final long min = TimeUnit.MILLISECONDS.toMinutes(l - TimeUnit.HOURS.toMillis(hr));
        final long sec = TimeUnit.MILLISECONDS.toSeconds(l - TimeUnit.HOURS.toMillis(hr) - TimeUnit.MINUTES.toMillis(min));
        final long ms = TimeUnit.MILLISECONDS.toMillis(l - TimeUnit.HOURS.toMillis(hr) - TimeUnit.MINUTES.toMillis(min) - TimeUnit.SECONDS.toMillis(sec));
        return String.format("%02d:%02d:%02d.%03d", hr, min, sec, ms);
    }

    public static void main(final String[] args)
    {
        System.out.println(formatInterval(Long.parseLong(args[0])));
    }
}

The output will be formatted something like this

13:00:00.000

A shorter way to do this is to use the DurationFormatUtils class in Apache Commons Lang :

public static String formatTime(long millis) {
    return DurationFormatUtils.formatDuration(millis, "HH:mm:ss.S");
}

Why not this?

public static String GetFormattedInterval(final long ms) {
    long millis = ms % 1000;
    long x = ms / 1000;
    long seconds = x % 60;
    x /= 60;
    long minutes = x % 60;
    x /= 60;
    long hours = x % 24;

    return String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis);
}

This is the first bit of Joda work I've done where it seemed more tedious than the JDK support. A Joda implementation for the requested format (making a few assumptions about zero fields) is:

public void printDuration(long milliSecs)
{
    PeriodFormatter formatter = new PeriodFormatterBuilder()
        .printZeroIfSupported()
        .appendHours()
        .appendSeparator(":")
        .minimumPrintedDigits(2)
        .appendMinutes()
        .appendSeparator(":")
        .appendSecondsWithMillis()
        .toFormatter();

    System.out.println(formatter.print(new Period(milliSecs)));
}

Here's how I've done it, using only the standard JDK (this will work as far back as Java 1.1 by changing StringBuilder back to StringBuffer ):

static public String formatMillis(long val) {
    StringBuilder                       buf=new StringBuilder(20);
    String                              sgn="";

    if(val<0) { sgn="-"; val=Math.abs(val); }

    append(buf,sgn,0,(val/3600000)); val%=3600000;
    append(buf,":",2,(val/  60000)); val%=  60000;
    append(buf,":",2,(val/   1000)); val%=   1000;
    append(buf,".",3,(val        ));
    return buf.toString();
    }

/** Append a right-aligned and zero-padded numeric value to a `StringBuilder`. */
static private void append(StringBuilder tgt, String pfx, int dgt, long val) {
    tgt.append(pfx);
    if(dgt>1) {
        int pad=(dgt-1);
        for(long xa=val; xa>9 && pad>0; xa/=10) { pad--;           }
        for(int  xa=0;   xa<pad;        xa++  ) { tgt.append('0'); }
        }
    tgt.append(val);
    }

Reviewing the other answers, I came up with this function...

public static String formatInterval(final long interval, boolean millisecs )
{
    final long hr = TimeUnit.MILLISECONDS.toHours(interval);
    final long min = TimeUnit.MILLISECONDS.toMinutes(interval) %60;
    final long sec = TimeUnit.MILLISECONDS.toSeconds(interval) %60;
    final long ms = TimeUnit.MILLISECONDS.toMillis(interval) %1000;
    if( millisecs ) {
        return String.format("%02d:%02d:%02d.%03d", hr, min, sec, ms);
    } else {
        return String.format("%02d:%02d:%02d", hr, min, sec );
    }
}

Here is what's going on. When you pass milliseconds, that number is relative to Jan 1st, 1970. When you pass 0, it takes that date and converts it to your local time zone. If you are in Central time then that happens to be 7PM. If you run this then it all makes sense.

new SimpleDateFormat().format(0) => 12/31/69 7:00 PM

Edit, I think what you want to do is get elapsed time. For this I recommend using JodaTime which already does this pretty well. You do something like

PeriodFormatter formatter = new PeriodFormatterBuilder()
    .appendHours()
    .appendSuffix(" hour", " hours")
    .appendSeparator(" and ")
    .appendMinutes()
    .appendSuffix(" minute", " minutes")
    .appendSeparator(" and ")
    .appendSeconds()
    .appendSuffix(" second", " seconds")
    .toFormatter();

String formattedText = formatter.print(new Period(elapsedMilliSeconds));

Here's another way to do it. Fully self-contained and fully backwards-compatible. Unlimited number of days.

private static String slf(double n) {
  return String.valueOf(Double.valueOf(Math.floor(n)).longValue());
}

public static String timeSpan(long timeInMs) {
  double t = Double.valueOf(timeInMs);
  if(t < 1000d)
    return slf(t) + "ms";
  if(t < 60000d)
    return slf(t / 1000d) + "s " +
      slf(t % 1000d) + "ms";
  if(t < 3600000d)
    return slf(t / 60000d) + "m " +
      slf((t % 60000d) / 1000d) + "s " +
      slf(t % 1000d) + "ms";
  if(t < 86400000d)
    return slf(t / 3600000d) + "h " +
      slf((t % 3600000d) / 60000d) + "m " +
      slf((t % 60000d) / 1000d) + "s " +
      slf(t % 1000d) + "ms";
  return slf(t / 86400000d) + "d " +
    slf((t % 86400000d) / 3600000d) + "h " +
    slf((t % 3600000d) / 60000d) + "m " +
    slf((t % 60000d) / 1000d) + "s " +
    slf(t % 1000d) + "ms";
}

tl;dr

LocalTime                     // Represents a time-of-day, without date and without time zone.
.ofNanoOfDay(                 // Convert a count of nanoseconds into a time-of-day in a generic 24-hour day, ignoring real-world anomalies such as Daylight Saving Time (DST). 
    Duration                  // Class that represents a span-of-time not attached to the timeline.
    .of( milliseconds )       // Parse your count of milliseconds as a span-of-time.
    .toNanos()                // Extract total number of nanoseconds in this span-of-time.
)                             // Returns a `LocalDate` object.
.toString()                   // Generate text in standard ISO 8601 format for a time-of-day (*not* recommended by me). 

java.time.Duration

The proper class to represent a span-of-time unattached to the timeline with a scale of hours-minutes-seconds is Duration . For a scale of years-months-days, use Period .

Duration d = Duration.of( milliseconds ) ;

ISO 8601 format

I suggest you avoid reporting a span-of-time using the time-of-day format of HH:MM:SS. I have seen the inherent ambiguity lead to misinterpretation and confusion in real-world business apps.

There is a standard for reporting such values, defined in ISO 8601 : PnYnMnDTnHnMnS . The P marks the beginning while the T separates any year-month-day portion from any hour-minute-second portion.

The java.time classes use the ISO 8601 standard formats by default when parsing/generating strings. This includes the Duration class.

Duration d = Duration.ofMillis( 5_025_678L ) ;
String output = d.toString() ;

See this code run live at IdeOne.com .

PT1H23M45.678S

These ISO 8601 strings can be parsed as well as generated.

Duration d = Duration.parse( "PT1H23M45.678S" ) ;

Time-of-day format

But if you insist on use time-of-day format for your duration, you can put together such a sting by calling the to…Part methods on Duration object.

Duration d = Duration.ofMillis( 5_025_678L ) ;
String output = d.toHoursPart() + ":" + d.toMinutesPart() + ":" + d.toSecondsPart() + "." + TimeUnit.NANOSECONDS.toMillis( d.toNanosPart() ) ;

1:23:45.678

Or we could abuse the LocalTime class to create your string.

Duration d = Duration.ofMillis( 5_025_678L ) ;
long nanoOfDay = d.toNanos() ;
LocalTime localTimeBogus = LocalTime.ofNanoOfDay( nanoOfDay ) ;
String output = localTimeBogus.toString() ;

Again, you can see this code run live at IdeOne.com .

01:23:45.678

org.apache.commons.lang.time.DurationFormatUtils.formatDuration(stopWatch.getTime(), "HH:mm:ss")

The format happens according to your local timezone, so if you pass 0, it assumes 0 GMT and then converts it in your local timezone.

What you are trying to format is a duration, not a Date-Time

You have a duration (no. of milliseconds elapsed) which is different from a Date-Time ie an instant on timeline. You use SimpleDateFormat to format a java.util.Date which represents an instant on the timeline. The difference can be better understood by the following examples:

  1. This machine has been running since 2021-09-30'T'10:20:30.
  2. This machine has been running for 3 days 4 hours 5 minutes.

The former represents an instant on timeline while the later represents a duration.

java.time

The java.util Date-Time API and their formatting API, SimpleDateFormat are outdated and error-prone. It is recommended to stop using them completely and switch to the modern Date-Time API * .

Also, quoted below is a notice from the home page of Joda-Time :

Note that from Java SE 8 onwards, users are asked to migrate to java.time (JSR-310) - a core part of the JDK which replaces this project.

Solution using java.time , the modern Date-Time API: I recommend you use java.time.Duration which is modelled on ISO-8601 standards and was introduced with Java-8 as part of JSR-310 implementation . With Java-9 some more convenience methods were introduced.

Demo:

import java.time.Duration;

public class Main {
    public static void main(String[] args) {
        // A sample input
        long millis = 123456789L;
        Duration duration = Duration.ofMillis(millis);
        // Default format
        System.out.println(duration);

        // Custom format
        // ####################################Java-8####################################
        String formattedElapsedTime = String.format("%2d:%02d:%02d.%d", duration.toHours() % 24,
                duration.toMinutes() % 60, duration.toSeconds() % 60, duration.toMillis() % 1000);
        System.out.println(formattedElapsedTime);
        // ##############################################################################

        // ####################################Java-9####################################
        formattedElapsedTime = String.format("%2d:%02d:%02d.%d", duration.toHoursPart(), duration.toMinutesPart(),
                duration.toSecondsPart(), duration.toMillisPart());
        System.out.println(formattedElapsedTime);
        // ##############################################################################
    }
}

Output:

PT34H17M36.789S
10:17:36.789
10:17:36.789

ONLINE DEMO

Learn more about the modern Date-Time API * from Trail: Date Time .


* 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 . Android 8.0 Oreo already provides support for java.time .

Using a plain Java Calendar for intervals up to one day (24 hours) see my answer to the question: How to format time intervals in Java?

Variant: Up to 24 hours

Simple formatting for elapsed time less than 24h. Over 24h the code will only display the hours within the next day and won't add the elapsed day to the hours.

public static String formatElapsedTime(long milliseconds) {

    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));

    return sdf.format(milliseconds);
}

Missing features in sample code:

  • Eliminate the timezone with "UTC"
  • Use the 24h format "HH"

Variant: Over 24 hours

public static String formatElapsedTimeOver24h(long milliseconds) {

    // Compiler will take care of constant arithmetics
    if (24 * 60 * 60 * 1000 > milliseconds) {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));

        return sdf.format(milliseconds);

    } else {
        SimpleDateFormat sdf = new SimpleDateFormat(":mm:ss.SSS");
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));

        // Keep long data type
        // Compiler will take care of constant arithmetics
        long hours = milliseconds / (60L * 60L * 1000L);

        return hours + sdf.format(milliseconds);
    }
}

This one actually works, but it seems like I'm tweaking the intent of the method:-).

public static String formatTime(long millis) {
    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");

    String strDate = sdf.format(millis - 3600000);
    return strDate;
}

For those of you who really knows how this works you'll probably find some caveats.

You can use formatElapsedTime or formatSameDayTime methods in DateUtils class.

This method must work in all not too old versions of Java )

public static String formatNanoSeconds(long duration) {
    StringBuilder sb = new StringBuilder(20);
    long quotient = duration / 1_000_000;
    long remainder = quotient % 1_000;
    sb.insert(0, String.format(".%03d", remainder));
    quotient = quotient / 1_000;
    remainder = quotient % 60;
    sb.insert(0, String.format(":%02d", remainder));
    quotient = quotient / 60;
    remainder = quotient % 60;
    sb.insert(0, String.format(":%02d", remainder));
    quotient = quotient / 60;
    remainder = quotient % 24;
    sb.insert(0, String.format(" %02d", remainder));
    quotient = quotient / 24;
    sb.insert(0, String.format("%d", quotient));
    return sb.toString();
}

StringBuilder.insert() , though fulfills array copy, must still work better than string concatenation.

And here's the test:

@Test
void formatNanoSeconds() {
    long m = 1_000_000;
    assertEquals("0 00:00:00.000", formatNanoSeconds(0));
    assertEquals("0 00:00:00.000", formatNanoSeconds(1));
    assertEquals("0 00:00:00.000", formatNanoSeconds(999_999));
    assertEquals("0 00:00:00.001", formatNanoSeconds(1_000_000));
    assertEquals("0 00:00:00.999", formatNanoSeconds(999 * m));
    assertEquals("0 00:00:01.000", formatNanoSeconds(1000 * m));
    assertEquals("0 00:00:59.000", formatNanoSeconds(59_000 * m));
    assertEquals("0 00:01:00.000", formatNanoSeconds(60_000 * m));
    assertEquals("0 00:01:01.000", formatNanoSeconds(61_000 * m));
    assertEquals("0 00:59:00.000", formatNanoSeconds(59 * 60_000 * m));
    assertEquals("0 01:00:00.000", formatNanoSeconds(60 * 60_000 * m));
    assertEquals("0 01:01:00.000", formatNanoSeconds(61 * 60_000 * m));
    assertEquals("0 23:00:00.000", formatNanoSeconds(23 * 60 * 60_000 * m));
    assertEquals("1 00:00:00.000", formatNanoSeconds(24 * 60 * 60_000 * m));
    assertEquals("1 01:00:00.000", formatNanoSeconds(25 * 60 * 60_000 * m));
    assertEquals("125 17:28:58.819", formatNanoSeconds(
            ((((125L * 24 * 3600) + (17 * 3600) + (28 * 60) + 58) * 1000) + 819) * m + 1));
    assertEquals("90 08:05:04.009", formatNanoSeconds(
            ((((90L * 24 * 3600) + (8 * 3600) + (5 * 60) + 4) * 1000) + 9) * m + 358));
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM