How can I "pretty print" a Duration in Java?
Asked Answered
J

13

155

Does anyone know of a Java library that can pretty print a number in milliseconds in the same way that C# does?

E.g., 123456 ms as a long would be printed as 4d1h3m5s.

Jackson answered 12/8, 2010 at 19:42 Comment(6)
FYI, the format you seem to be describing is a Duration defined in the sensible standard, ISO 8601: PnYnMnDTnHnMnS where P means "Period" and marks the beginning, T separates the date portion from time portion, and in between are optional occurrances of a number with a single-letter abbreviation. For example, PT4H30M = four and a half hours.Longitudinal
If all else fails it's a very simple matter to do it yourself. Just use successive applications of % and / to split the number into parts. Almost easier than some of the proposed answers.Shandy
@HotLicks Leave it to library methods for much cleaner and clearer code than using / and %.Unfasten
Yes, in the intervening 8 years since I asked this question (!) I've moved over to joda time which suits my use case very wellJackson
@Jackson The Joda-Time project is now in maintenance-mode, and advises migration to the java.time classes built into Java 8 and later.Longitudinal
Does this answer your question? How to format a duration in java? (e.g format H:MM:SS)Inotropic
B
93

Joda Time has a pretty good way to do this using a PeriodFormatterBuilder.

Quick Win: PeriodFormat.getDefault().print(duration.toPeriod());

e.g.

//import org.joda.time.format.PeriodFormatter;
//import org.joda.time.format.PeriodFormatterBuilder;
//import org.joda.time.Duration;

Duration duration = new Duration(123456); // in milliseconds
PeriodFormatter formatter = new PeriodFormatterBuilder()
     .appendDays()
     .appendSuffix("d")
     .appendHours()
     .appendSuffix("h")
     .appendMinutes()
     .appendSuffix("m")
     .appendSeconds()
     .appendSuffix("s")
     .toFormatter();
String formatted = formatter.print(duration.toPeriod());
System.out.println(formatted);
Bores answered 12/8, 2010 at 19:44 Comment(4)
It appears from an answer below that an instance of Period can be created directly, without first creating a Duration instance and then converting it to Period. E.g. Period period = new Period(millis); String formatted = formatter.print(period);Candescent
Beware of this "duration.toPeriod()" conversion. If the duration is quite big, the day portion onwards would remain as 0. The hours portion will keep growing. You will get 25h10m23s but never get the "d". The reason is there is no fully correct way to convert hours to days in Joda's strict ways. Most of the cases, if you are comparing two instants and wanting to print it, you can do new Period(t1, t2) instead of new Duration(t1, t2).toPeriod().Vulturine
@Vulturine do you know how to convert duration to period with respect to days? I use duration from which I can substitute second in my on my timer. Event setting PeriodType.daysTime() or .standard() didn't helpStrachan
@Strachan If you really want to, and can accept the standard definition of a day, then you can "formatter.print(duration.toPeriod().normalizedStandard())" in the code above. Have fun!Vulturine
V
142

I've built a simple solution, using Java 8's Duration.toString() and a bit of regex:

public static String humanReadableFormat(Duration duration) {
    return duration.toString()
            .substring(2)
            .replaceAll("(\\d[HMS])(?!$)", "$1 ")
            .toLowerCase();
}

The result will look like:

- 5h
- 7h 15m
- 6h 50m 15s
- 2h 5s
- 0.1s

If you don't want spaces between, just remove replaceAll.

Violoncellist answered 8/11, 2016 at 12:54 Comment(10)
You should never rely on the output of toStrong() because this may change in further versions.Sutherlan
@Sutherlan it's not likely to change, since it's specified in the javadocValora
@Sutherlan Unlikely to change as the output of Duration::toString is formatted according to the well-defined and well-worn ISO 8601 standard.Longitudinal
If you don't want fractions, add .replaceAll("\\.\\d+", "") before the call to toLowerCase().Liquidator
@Sutherlan Generally when something may change in future versions they explicitly mention it in the Javadoc or don't document it at all. Since they don't do that, we shouldn't assume it is subject to change otherwise where do we draw the line? Many things are documented and sure, there's always the miniscule chance they change their mind, but since they do mention when things shouldn't be relied on then it's safe to assume it won't change.Pisarik
That's simply not true. I had the experience with a legacy system: From one Java version to the next (I guess it was 1.5 to 1.6) the number of fraction digits from Float#toString() changed from 4 to 3 digits. This prevented the company I worked for years to upgrade Java. Of course you can use toString(), but for other things than debugging or logging it is a bad architectural choice and should be avoided IMHO.Sutherlan
@Sutherlan Your point in generally correct and I wouldn't myself rely on the toString() output of most classes. But in this very specific case, the method definition states that its output follows the ISO-8601 standard, as you can see in the method's Javadoc. So it's not an implementation detail like in most classes, therefore not something I would expect a language as mature as Java to change like that,Violoncellist
How would you add days in the above solution?Histo
@Sutherlan by that logic, you should never rely on the output of any third-party method.Shrive
@Violoncellist yes, ok. In this particular case. Since exceptions are not that good, I would prefer another method named accordingly.Sutherlan
B
93

Joda Time has a pretty good way to do this using a PeriodFormatterBuilder.

Quick Win: PeriodFormat.getDefault().print(duration.toPeriod());

e.g.

//import org.joda.time.format.PeriodFormatter;
//import org.joda.time.format.PeriodFormatterBuilder;
//import org.joda.time.Duration;

Duration duration = new Duration(123456); // in milliseconds
PeriodFormatter formatter = new PeriodFormatterBuilder()
     .appendDays()
     .appendSuffix("d")
     .appendHours()
     .appendSuffix("h")
     .appendMinutes()
     .appendSuffix("m")
     .appendSeconds()
     .appendSuffix("s")
     .toFormatter();
String formatted = formatter.print(duration.toPeriod());
System.out.println(formatted);
Bores answered 12/8, 2010 at 19:44 Comment(4)
It appears from an answer below that an instance of Period can be created directly, without first creating a Duration instance and then converting it to Period. E.g. Period period = new Period(millis); String formatted = formatter.print(period);Candescent
Beware of this "duration.toPeriod()" conversion. If the duration is quite big, the day portion onwards would remain as 0. The hours portion will keep growing. You will get 25h10m23s but never get the "d". The reason is there is no fully correct way to convert hours to days in Joda's strict ways. Most of the cases, if you are comparing two instants and wanting to print it, you can do new Period(t1, t2) instead of new Duration(t1, t2).toPeriod().Vulturine
@Vulturine do you know how to convert duration to period with respect to days? I use duration from which I can substitute second in my on my timer. Event setting PeriodType.daysTime() or .standard() didn't helpStrachan
@Strachan If you really want to, and can accept the standard definition of a day, then you can "formatter.print(duration.toPeriod().normalizedStandard())" in the code above. Have fun!Vulturine
A
32

Apache commons-lang provides the DurationFormatUtils class to get this done as well. It has several formatting methods like formatDurationHMS and formatDurationISO, but if you just want a nice output, use:

DurationFormatUtils.formatDurationWords(123456, true, true);

Result

2 minutes 3 seconds
Aquilegia answered 25/4, 2013 at 15:45 Comment(0)
B
20

Java 9+

Duration d1 = Duration.ofDays(0);
        d1 = d1.plusHours(47);
        d1 = d1.plusMinutes(124);
        d1 = d1.plusSeconds(124);
System.out.println(String.format("%s d %sh %sm %ss", 
                d1.toDaysPart(), 
                d1.toHoursPart(), 
                d1.toMinutesPart(), 
                d1.toSecondsPart()));

2 d 1h 6m 4s

Billbillabong answered 8/8, 2018 at 20:8 Comment(1)
How are you getting "toHoursPart" and above?Infinitude
S
19

org.threeten.extra.AmountFormats.wordBased

The ThreeTen-Extra project, which is maintained by Stephen Colebourne, the author of JSR 310, java.time, and Joda-Time, has an AmountFormats class which works with the standard Java 8 date time classes. It's fairly verbose though, with no option for more compact output.

Duration d = Duration.ofMinutes(1).plusSeconds(9).plusMillis(86);
System.out.println(AmountFormats.wordBased(d, Locale.getDefault()));

1 minute, 9 seconds and 86 milliseconds

Stash answered 19/4, 2019 at 2:1 Comment(3)
Wow, nice to know, I’ve not seen that feature. Has one major flaw though: Missing the Oxford comma.Longitudinal
@BasilBourque The Oxford comma is typical for US, but interestingly not for UK. My lib Time4J (which has a far better internationalization) supports this distinction in the class net.time4j.PrettyTime and also allows control over compact output if desired.Thorman
FYI, you can add an Oxford comma in ThreeTen-Extra by providing your own locale for the wordbased ResourceBundle, and overriding the WordBased.spaceandspace key. You could do this by putting a file named org/threeten/extra/wordbased_ox.properties on your ClassPath with contents WordBased.spaceandspace=, and . Then, if you use Locale.forLanguageTag("ox") the Oxford comma will be added automatically. Source is here: github.com/ThreeTen/threeten-extra/blob/master/src/main/…Phallicism
A
12

Another Java 9+ solution:

private String formatDuration(Duration duration) {
    List<String> parts = new ArrayList<>();
    long days = duration.toDaysPart();
    if (days > 0) {
        parts.add(plural(days, "day"));
    }
    int hours = duration.toHoursPart();
    if (hours > 0 || !parts.isEmpty()) {
        parts.add(plural(hours, "hour"));
    }
    int minutes = duration.toMinutesPart();
    if (minutes > 0 || !parts.isEmpty()) {
        parts.add(plural(minutes, "minute"));
    }
    int seconds = duration.toSecondsPart();
    parts.add(plural(seconds, "second"));
    return String.join(", ", parts);
}

private String plural(long num, String unit) {
    return num + " " + unit + (num == 1 ? "" : "s");
}

E.g.,

6 days, 21 hours, 2 minutes, 14 seconds
Augsburg answered 8/3, 2021 at 20:54 Comment(3)
You can also use StringBuilder or StringJoinerRevocation
The whole inside a class with JUnit test code after modifications: gist.github.com/dtonhofer/e2a0603a2eff556b37f4c07b20d193d3Radack
what about yeah month day? 2 years 11 months 28 daysWiggins
F
11

A Java 8 version based on user678573's answer:

private static String humanReadableFormat(Duration duration) {
    return String.format("%s days and %sh %sm %ss", duration.toDays(),
            duration.toHours() - TimeUnit.DAYS.toHours(duration.toDays()),
            duration.toMinutes() - TimeUnit.HOURS.toMinutes(duration.toHours()),
            duration.getSeconds() - TimeUnit.MINUTES.toSeconds(duration.toMinutes()));
}

... since there is no PeriodFormatter in Java 8 and no methods like getHours, getMinutes, ...

I'd be happy to see a better version for Java 8.

Fruition answered 25/6, 2018 at 14:37 Comment(4)
throws java.util.IllegalFormatConversionException: f != java.lang.LongBillbillabong
with Duration d1 = Duration.ofDays(0); d1 = d1.plusHours(47); d1 = d1.plusMinutes(124); d1 = d1.plusSeconds(124); System.out.println(String.format("%s days and %sh %sm %1.0fs", d1.toDays(), d1.toHours() - TimeUnit.DAYS.toHours(d1.toDays()), d1.toMinutes() - TimeUnit.HOURS.toMinutes(d1.toHours()), d1.toSeconds() - TimeUnit.MINUTES.toSeconds(d1.toMinutes())));Billbillabong
@Billbillabong Thanks for pointing that out. I think I was originally writing the code for groovy, so it might have worked in that language. I corrected it for java now.Fruition
what about yeah month day? 2 years 11 months 28 daysWiggins
S
10

JodaTime has a Period class that can represent such quantities, and can be rendered (via IsoPeriodFormat) in ISO8601 format, e.g. PT4D1H3M5S, e.g.

Period period = new Period(millis);
String formatted = ISOPeriodFormat.standard().print(period);

If that format isn't the one you want, then PeriodFormatterBuilder lets you assemble arbitrary layouts, including your C#-style 4d1h3m5s.

Sociability answered 12/8, 2010 at 19:44 Comment(1)
Just as a note, new Period(millis).toString() uses the ISOPeriodFormat.standard() by default.Wandering
H
10

Here's how you can do it using pure JDK code:

import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;

long diffTime = 215081000L;
Duration duration = DatatypeFactory.newInstance().newDuration(diffTime);

System.out.printf("%02d:%02d:%02d", duration.getDays() * 24 + duration.getHours(), duration.getMinutes(), duration.getSeconds()); 
Hustler answered 1/5, 2013 at 17:29 Comment(0)
T
10

An alternative to the builder-approach of Joda-Time would be a pattern-based solution. This is offered by my library Time4J. Example using the class Duration.Formatter (added some spaces for more readability - removing the spaces will yield the wished C#-style):

IsoUnit unit = ClockUnit.MILLIS;
Duration<IsoUnit> dur = // normalized duration with multiple components
  Duration.of(123456, unit).with(Duration.STD_PERIOD);
Duration.Formatter<IsoUnit> f = // create formatter/parser with optional millis
  Duration.Formatter.ofPattern("D'd' h'h' m'm' s[.fff]'s'");

System.out.println(f.format(dur)); // output: 0d 0h 2m 3.456s

This formatter can also print durations of java.time-API (however, the normalization features of that type are less powerful):

System.out.println(f.format(java.time.Duration.ofMillis(123456))); // output: 0d 0h 2m 3.456s

The expectation of the OP that "123456 ms as a long would be printed as 4d1h3m5s" is calculated in an obviously wrong way. I assume sloppiness as reason. The same duration formatter defined above can also be used as parser. The following code shows that "4d1h3m5s" rather corresponds to 349385000 = 1000 * (4 * 86400 + 1 * 3600 + 3 * 60 + 5):

System.out.println(
  f.parse("4d 1h 3m 5s")
   .toClockPeriodWithDaysAs24Hours()
   .with(unit.only())
   .getPartialAmount(unit)); // 349385000

Another way is using the class net.time4j.PrettyTime (which is also good for localized output and printing relative times like "yesterday", "next Sunday", "4 days ago" etc.):

String s = PrettyTime.of(Locale.ENGLISH).print(dur, TextWidth.NARROW);
System.out.println(s); // output: 2m 3s 456ms

s = PrettyTime.of(Locale.ENGLISH).print(dur, TextWidth.WIDE);
System.out.println(s); // output: 2 minutes, 3 seconds, and 456 milliseconds

s = PrettyTime.of(Locale.UK).print(dur, TextWidth.WIDE);
System.out.println(s); // output: 2 minutes, 3 seconds and 456 milliseconds

The text width controls if abbreviations are used or not. The list format can be controlled, too, by choosing the appropriate locale. For example, standard English uses the Oxform comma, while UK does not. The latest version v5.5 of Time4J supports more than 90 languages and uses translations based on the CLDR-repository (an industry standard).

Thorman answered 28/1, 2015 at 14:22 Comment(2)
this PrettyTime is way better than the one in the other answer! too bad I didn't find this first.Carmen
I don't know why there are now two downvotes for this well working solution so I have decided to extend the answer by giving more examples also about parsing (the opposite of formatting) and to highlight the wrong expectation of the OP.Thorman
N
9

With Java 8 you can also use the toString() method of java.time.Duration to format it without external libraries using ISO 8601 seconds based representation such as PT8H6M12.345S.

Negation answered 20/3, 2014 at 10:14 Comment(3)
Note that this does not print daysMaintain
Not sure this format meets my definition of pretty print. :-)Sweepstakes
@Sweepstakes To make it a little prettier, see the Answer by erickdeoliveiraleal.Longitudinal
R
2

I realize this might not fit your use case exactly, but PrettyTime might be useful here.

PrettyTime p = new PrettyTime();
System.out.println(p.format(new Date()));
//prints: “right now”

System.out.println(p.format(new Date(1000*60*10)));
//prints: “10 minutes from now”
Revis answered 1/5, 2013 at 18:11 Comment(2)
Is it possible to have just "10 minutes" ?Taeniacide
Not an answer - the question was about the Duration type, not Date.Kami
C
0

naive kotlin version (avoiding to*Part functions, to work with Android)

fun prettyFormatSecondsToDuration(seconds: Long): String {
    val duration = java.time.Duration.ofSeconds(seconds)
    return buildString {
        val hours = duration.toHours()
        if (hours > 0) append("${hours}h ")

        val minutes = duration.minusHours(hours).toMinutes()
        if (minutes > 0) append("${minutes}m ")

        val partSeconds = duration.minusHours(hours).minusMinutes(minutes).seconds
        if (partSeconds > 0) append("${partSeconds}s ")
    }.trimEnd()
}
Covenanter answered 14/3 at 13:52 Comment(1)
Thanks. I believe the toXxxPart methods are available on Android through desugaring.Unfasten

© 2022 - 2024 — McMap. All rights reserved.