5

How do I pretty print time duration in perl?

The only thing I could come up with so far is

my $interval = 1351521657387 - 1351515910623; # milliseconds
my $duration = DateTime::Duration->new(
    seconds => POSIX::floor($interval/1000) ,
    nanoseconds  => 1000000 * ($interval % 1000),
);
my $df = DateTime::Format::Duration->new(
    pattern => '%Y years, %m months, %e days, ' .
               '%H hours, %M minutes, %S seconds, %N nanoseconds',
    normalize => 1,
);
print $df->format_duration($duration);

which results in

0 years, 00 months, 0 days, 01 hours, 35 minutes, 46 seconds, 764000000 nanoseconds

This is no good for me for the following reasons:

  1. I don't want to see "0 years" (space waste) &c and I don't want to remove "%Y years" from the pattern (what if I do need years next time?)
  2. I know in advance that my precision is only milliseconds, I don't want to see the 6 zeros in the nanoseconds part.
  3. I care about prettiness/compactness/human readability much more than about precision/machine readability. I.e., I want to see something like "1.2 years" or "3.22 months" or "7.88 days" or "5.7 hours" or "75.5 minutes" (or "1.26 hours", whatever looks better to you) or "24.7 seconds" or "133.7 milliseconds" &c (similar to how R prints difftime)
sds
  • 58,617
  • 29
  • 161
  • 278

3 Answers3

3

You could build the pattern dynamically depending on the whether or not certain values are "true".

...
push @pattern, '%Y years' if $duration->year;
push @pattern, '%m months' if $duration->month;
...
my $df = DateTime::Format::Duration->new(
    pattern => join(', ', @pattern),
    normalize => 1,
);
print $df->format_duration($duration);
titanofold
  • 2,852
  • 1
  • 15
  • 21
2

Here is what I ended up using:

sub difftime2string ($) {
  my ($x) = @_;
  ($x < 0) and return "-" . difftime2string(-$x);
  ($x < 1) and return sprintf("%.2fms",$x*1000);
  ($x < 100) and return sprintf("%.2fsec",$x);
  ($x < 6000) and return sprintf("%.2fmin",$x/60);
  ($x < 108000) and return sprintf("%.2fhrs",$x/3600);
  ($x < 400*24*3600) and return sprintf("%.2fdays",$x/(24*3600));
  return sprintf("%.2f years",$x/(365.25*24*3600));
}
sds
  • 58,617
  • 29
  • 161
  • 278
0

I want to see something like "1.2 years" or "3.22 months" or "7.88 days"

You could use the constants in Time::Seconds:

use Time::Seconds;
use feature qw(say);
...

$time_seconds = $interval / 1000;
if ( $time_seconds > ONE_YEAR ) {
    printf "The interval is %.2f years\n", $time_seconds / ONE_YEAR;
}
else {
if ( $time_seconds > ONE_DAY ) {
    printf "The interval is %.2f days\n", $time_seconds / ONE_DAY;
}
else { 
if ( $time_seconds > ONE_HOUR ) {
    printf "The interval is %.2f hours\n", $time_seconds / ONE_HOUR;
}
else {
    say "The interval is $time_seconds seconds";
}

A switch can also be used, but it's still marked as experimental;

use feature qw(switch say);
use Time::Seconds;

...
my $time_seconds = $interval / 1000;

for ( $time_seconds ) {
    when ( $time_seconds > ONE_YEAR ) {
        printf "The interval is %.2f years\n", $time_seconds / ONE_YEAR;
    }
    when ( $time_seconds > ONE_DAY ) {
        printf "The interval is %.2f days\n", $time_seconds / ONE_DAY;
    }
    when ( $time_seconds > ONE_HOUR ) {
        printf "The interval is %.2f hours\n", $time_seconds / ONE_HOUR;
    }
    default { say "The interval is $time_seconds seconds"; }
}

There may even be a way of combining everything into an array in order to have a single Time statement. (Untested, but you get the idea):

 my @times = (
    [ INTERVAL => ONE_YEAR, VALUE => "years" ],
    [ INTERVAL => ONE_DAY,  VALUE => "days"  ],
    [ INTERVAL => ONE_HOUR, VALUE => "hours" ],
);

for my $interval ( @times ) {
    if ( $time_seconds > $interval->{INTERVAL} ) {
       printf "The interval is %.2f %s\n"
          , $time_seconds / $interval->{INTERVAL}, $interval->{VALUE};
    }
}

Not too crazy about that. You're better off simply making a pretty_time subroutine to hide the code.

say pretty_time( $interval );
David W.
  • 105,218
  • 39
  • 216
  • 337