如何在Java中以hh:mm:ss.SSS格式格式化经过的时间间隔?

41

我正在制作一个秒表,使用Java的SimpleDateFormat将毫秒数转换为漂亮的“hh:mm:ss:SSS”格式。问题在于小时字段总是有一些随机数。这是我正在使用的代码:

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

    String strDate = sdf.format(millis);
    return strDate;
}
如果我去掉hh部分,那么它就可以正常工作。否则,在hh部分,即使传入的参数是零毫秒,它也会显示类似于“07”的随机内容。
我不太了解SimpleDateFormat类。感谢任何帮助。

4
根据这个可以推断出你居住在美国中部地区。 - Ed Staub
使用24小时制的HH代替hh。删除时区sdf.setTimeZone(TimeZone.getTimeZone("UTC"))。请参见下面的答案。 - notes-jj
类似问题 https://dev59.com/vm435IYBdhLWcg3wfQF9 - Alex78191
17个回答

87

最新的JDK中内置了一个鲜为人知的类TimeUnit,可支持您想要做的操作。

您需要使用java.util.concurrent.TimeUnit来处理时间间隔

SimpleDateFormat正如其名,它格式化java.util.Date的实例,或在您的情况下将long值转换为java.util.Date上下文,但它不知道如何处理您正在处理的时间间隔

您无需使用外部库(例如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])));
    }
}

输出将会以类似以下的格式进行格式化。
13:00:00.000

注意:此解决方案需要Java 6或更高版本。 - Lawrence Dol
4
这个解决方案中的'l'看起来很像数字'1'。我建议使用类似于'mstime'的变量名来表示它。 - ingyhere
2
此外,我喜欢它,但我不确定它是否比写下以下代码更美观:long elapsedTimeSeconds = elapsedTimeMillis / 1000; String.format("%02d:%02d:%02d.%03d", elapsedTimeSeconds / 3600, elapsedTimeSeconds % 3600) / 60, (elapsedTimeSeconds % 60), elapsedTimeMillis % 1000); - ingyhere
丑陋的解决方案,看看mksteve的答案。 - badadin

36

一个更短的方法是使用DurationFormatUtils类,它位于Apache Commons Lang中:

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

7
为什么不用这个?
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);
}

6

这是我在Joda方面做的第一个工作,感觉比JDK支持更加繁琐。一个关于所需格式的Joda实现(对零字段进行了一些假设)如下:

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)));
}

2
FYI,Joda-Time 项目现已进入维护模式,建议迁移到java.time类。请参阅Oracle的教程 - Basil Bourque

6

以下是我使用标准JDK完成的方法(通过将StringBuilder改回StringBuffer,可以在Java 1.1及更早版本中使用):

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);
    }

在2011年,使用TimeUnit会更加简洁。 - user177800
在当今,使用JodaTime可能会更加简洁。 - Amir Raminfar
7
这不是解决这个问题的惯用Java方式。标准库中已经内置了SimpleDateFormatTimeUnit以解决这些问题,而无需包含几兆字节的第三方库或编写所有的数学和字符串操作代码。即使没有其他原因,使用SimpleDateFormat的好处在于其能透明地处理时区信息。 - user177800
3
@Jarrod:首先,对于已经过去的时间,时区是无关紧要的,并且SimpleDateFormat不是正确的类,其次,如果你必须针对较旧的虚拟机版本,TimeUnit就不是一个选项。 - Lawrence Dol
这可能是我见过的最难读的代码。不是在谈论复杂性,而是纯粹的“只要编译通过就好”的态度。 - Buffalo
@Buffalo:YETYO,但你不了解我,我的想法或态度。 - Lawrence Dol

5

经过对其他回答的审查,我得出了这个函数...

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 );
    }
}

3

以下是相关内容。当传递以毫秒为单位的数字时,该数字相对于1970年1月1日。当您传递0时,它会将该日期转换为您的本地时区。如果您在中央时区,则为晚上7点。如果您运行此程序,则所有内容都会变得清晰明了。

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

编辑,我认为你希望获得经过的时间。对于这个问题,我建议使用JodaTime,它已经很好地解决了这个问题。你可以使用类似下面的方法:

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));

夏令时,因此是CDT,而不是EDT。 - Ed Staub
我同意,SimpleDateFormat 不是设计用来格式化经过的时间的。更容易编写自己的格式化程序或使用您推荐的 Joda 格式化程序。 - Chris

2
您要格式化的是一个持续时间,而不是日期时间。
您有一个持续时间(经过的毫秒数),它与日期时间即时间轴上的瞬间不同。您使用SimpleDateFormat来格式化java.util.Date,它表示时间轴上的瞬间。可以通过以下示例更好地理解差异:
1.此计算机自2021-09-30'T'10:20:30以来一直在运行。 2.此计算机已运行3天4小时5分钟。
前者代表时间轴上的瞬间,而后者代表持续时间。

java.time

“java.util”日期时间API及其格式化API“SimpleDateFormat”已经过时且容易出错。建议完全停止使用它们并切换到现代日期时间API*
此外,以下是来自Joda-Time首页的通知:
请注意,从Java SE 8开始,用户被要求迁移到java.time(JSR-310)——JDK的核心部分,用于替代该项目。

使用现代的日期时间API-java.time的解决方案:我建议您使用java.time.Duration,它是基于ISO-8601标准建模的,并作为JSR-310实现的一部分在Java-8中引入。随着Java-9的推出,还引入了更多的便利方法。

演示:

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);
        // ##############################################################################
    }
}

输出:

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

在线演示

教程:日期时间了解更多关于现代日期时间API*的内容。


* 如果由于任何原因,您必须坚持使用Java 6或Java 7,则可以使用{{link1:ThreeTen-Backport}}将大多数java.time功能回退到Java 6和7。如果您正在为Android项目工作,并且您的Android API级别仍不符合Java-8,请检查{{link2:通过desugaring可用的Java 8+ API}}和{{link3:如何在Android项目中使用ThreeTenABP}}。Android 8.0 Oreo已经提供了{{link4:java.time支持}}。


2

这里有另一种方法来实现它。完全自包含且完全向后兼容。天数没有限制。

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";
}

最佳解决方案是,如果您不希望输出包含可能在开头包含0的完整流逝时间。我想要的格式是“50秒前”或“1小时2分钟5秒前”,而不是完整的“HH:mm:ss.S”格式。 - bickster

1

时间格式按照您的本地时区进行,因此如果您传递了0,则假定为0格林尼治标准时间(GMT),然后将其转换为您的本地时区。


是的,这基本上就是正在发生的事情。 - Amir Raminfar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接