Perl:如何漂亮地打印时间差(持续时间)

问题描述 投票:0回答:4

如何在 Perl 中漂亮地打印持续时间?

到目前为止我唯一能想到的是

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

导致

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

这对我来说没有好处,原因如下:

  1. 我不想看到“0年”(空间浪费)&c,并且我不想从pattern中删除“
    %Y年
    ”(如果下次我确实需要几年怎么办?)
  2. 我事先知道我的精度只有毫秒,我不想看到纳秒部分的6个零。
  3. 我更关心美观/紧凑/人类可读性而不是精度/机器可读性。也就是说,我想看到类似“1.2年”或“3.22个月”或“7.88天”或“5.7小时”或“75.5分钟”(或“1.26小时”) ,无论你看起来更好)或“24.7秒”或“133.7毫秒”&c(类似于R打印difftime
perl datetime duration datediff
4个回答
3
投票

您可以根据某些值是否为“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);

2
投票

这是我最终使用的:

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

0
投票

我想看到类似“1.2 年”或“3.22 个月”或“7.88 天”的内容

您可以使用 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";
}

也可以使用开关,但它仍然标记为实验性

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

甚至可能有一种方法将所有内容组合到一个数组中,以便拥有单个 Time 语句。 (未经测试,但你明白了):

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

对此不太疯狂。您最好简单地创建一个

pretty_time
子例程来 隐藏代码。

say pretty_time( $interval );

0
投票

因为我以为我问了一个类似的问题(另外还涉及一个参考时间点),但我自己找不到答案了(也许它被删除了),我劫持了这个问题来呈现我的答案(2024 -07-16)。 它有点像一个工具包,因此您必须根据您的用例进行调整。

所以它作为一个自我测试模块出现,希望是正确的(不过你可能不喜欢这种风格):

#!/usr/bin/perl
# written for SLES12 SP5 and PERL 5.18 by U. Windl in 2024
require 5.018_000;
use warnings;
use strict;

my $verbosity = 0;                      # verbosity

use constant REF_TYPE_CODE      => 'CODE';      # ref type for code

# other constants
use constant SECS_PER_MIN               => 60;          # seconds per minute
use constant SECS_PER_HOUR              => 60 * SECS_PER_MIN;   # .. per hour
use constant SECS_PER_DAY               => 24 * SECS_PER_HOUR;  # .. per day
use constant SECS_PER_WEEK              =>  7 * SECS_PER_DAY;   # .. per week

use constant _SECS_PER_MONTH    => SECS_PER_DAY * 31;   # inexact!
use constant _SECS_PER_YEAR     => SECS_PER_DAY * 366;  # inexact!
use constant MINS_PER_HOUR      => 60;  # minutes per hour
use constant HOURS_PER_DAY      => 24;  # hours per day (no DST!)
use constant MONTHS_PER_YEAR    => 12;  # months per year

# days in given month
use constant DAYS_IN_MONTH      => [31, 28, 31, 30, 31, 30, 31,
                                    31, 30, 31, 30, 31];        # days in month

use constant DURATION_UNITS => [
    [_SECS_PER_YEAR, 'Y'],
    [_SECS_PER_MONTH, 'm'],
    [SECS_PER_WEEK, 'W'],
    [SECS_PER_DAY, 'D'],
    [0, 'T'],                           # special: time following
    [SECS_PER_HOUR, 'H'],
    [SECS_PER_MIN, 'M'],
    [1, 'S']
];

# conditionally print verbose message
sub verbose($@)
{
    my ($level, @args) = @_;
    my $fh = $level <= 0 ? \*STDERR : \*STDOUT;

    print $fh join(' ', "[$level]",
                   map {
                       my $r = ref($_);

                       (!$r || $r ne REF_TYPE_CODE) ? $_ : $_->();
                   }
                   @args), "\n"
        if ($verbosity >= $level);
}

sub days_in_month($$)
{
    my ($year, $mindex) = @_;

    $mindex += 12, --$year
        if ($mindex < 0);               # previous month of January is December
    return DAYS_IN_MONTH->[$mindex]
        if ($mindex != 1 ||
            !($year % 400 == 00 || ($year % 4 == 0 && $year % 100 != 0)));
    return 29;
}

# days in given year
sub days_in_year($)
{
    my $year = $_[0];

    return 365 - DAYS_IN_MONTH->[1] + days_in_month($year, 1);
}

use POSIX qw(floor fmod);

# Duration as components skipping units larger than max_unit or units smaller
# than min_units.  For durations exceeding one month the anchor is used as a
# calendar reference point
sub duration_components($;$$$)
{
    my ($secs, $max_unit, $min_unit, $anchor) = @_;
    my $sign = $secs >= 0 ? '+' : '-';
    my @result = ($sign);
    my $me = 'duration_components';
    my $need_more = 1;                  # at least one unit must follow

    if (defined $min_unit) {            # convert name to number
        my $v;

        foreach (@{(DURATION_UNITS)}) {
            if ($_->[1] eq $min_unit) {
                $v = $_->[0];
                last;
            }
        }
        verbose(-1, "${me}:", 'unknown min_unit', $min_unit)
            unless (defined $v);
        $min_unit = $v;
    }
    $min_unit //= DURATION_UNITS->[-1]->[0];
    if (defined $max_unit) {            # convert name to number
        my $v;

        foreach (@{(DURATION_UNITS)}) {
            if ($_->[1] eq $max_unit) {
                $v = $_->[0];
                last;
            }
        }
        verbose(-1, "${me}:", 'unknown max_unit', $max_unit)
            unless (defined $v);
        $max_unit = $v;
    }
    $max_unit //= DURATION_UNITS->[0]->[0];
    verbose(-1, "${me}:",  'inconsistent max_unit', $max_unit, 'and min_unit',
            $min_unit)
        unless ($min_unit <= $max_unit);
    $secs = -$secs
        if ($secs < 0);
    if ($secs >= 4 * SECS_PER_WEEK) {   # special handling for months and more
        my (@tm0, @tm1);
        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $is_dst);

        $anchor //= time();
        verbose(5, "${me}:", "anchor=$anchor", "#secs=$secs",
                sub () { scalar(localtime $anchor) });
        @tm0 = localtime($anchor);
        $sign = $sign eq '-' ? -1 : 1;  # convert to number
        $anchor += $sign * $secs;
        verbose(6, "${me}:", "anchor=$anchor",
                sub () { scalar(localtime $anchor) });
        @tm1 = localtime($anchor);
        # to preserve @tm1, put the difference in @tm0 ("a - b" is "-(b - a)")
        foreach (0 .. 7) {
            verbose(7, "${me}:", "\$tm0[$_]=$tm0[$_]", "\$tm1[$_]=$tm1[$_]");
            $tm0[$_] -= $tm1[$_];
            $tm0[$_] *= -$sign;
            verbose(7, "${me}:", "\$tm0[$_]=$tm0[$_]");
        }
        ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $is_dst) = @tm0;
        # handle "carry ripple"
        verbose(6, "${me}:",
                "($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$is_dst)",
                "#secs=$secs");
        if ($sec < 0) {
            $sec += SECS_PER_MIN, --$min;
            verbose(7, "${me}:", "#sec=$sec", "#min=$min");
        }
        if ($min < 0) {
            $min += MINS_PER_HOUR, --$hour;
            verbose(7, "${me}:", "#min=$min", "#hour=$hour");
        }
        if ($hour < 0) {                # DST specialties 23h/25h ignored
            $hour += HOURS_PER_DAY, --$mday;
            verbose(7, "${me}:", "#hour=$hour", "#mday=$mday");
        }
        if ($mday < 0) {
            $tm0[4] -= $sign;           # one month less
            $mday += days_in_month(1900 + $tm1[5], $tm1[4]), --$mon;
            verbose(7, "${me}:", "#mday=$mday", "mon=$mon",
                    "year=$tm1[5]", "mon=$tm1[4]");
        }
        if ($mon < 0) {
            $mon += MONTHS_PER_YEAR, --$year;
            verbose(7, "${me}:", "#mon=$mon", "#year=$year");
        }
        verbose(5, "${me}:",
                "($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$is_dst)",
                "secs=$secs");
        verbose(-1, "${me}: year underflow:",
                "($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$is_dst)")
            if ($year < 0);
        if ($year > 0) {
            if ($max_unit >= _SECS_PER_YEAR) {
                my $m = $tm1[4];                # remember month

                push(@result, [$year, 'Y']);
                while ($year-- > 0) {
                    my $days;

                    --$tm1[5]           # previous year
                        if ($sign > 0);
                    $days = days_in_year(1900 + $tm1[5]);
                    $secs -= SECS_PER_DAY * $days;
                    verbose(7, "${me}:",
                            "#year=$year", "#days=$days", "#secs=$secs",
                            "year=$tm1[5]");
                    ++$tm1[5]           # next year
                        if ($sign < 0);
                }
                if ($min_unit >= _SECS_PER_YEAR) {
                    my $n = floor(1000 * $secs / _SECS_PER_YEAR) / 1000;

                    verbose(6, "${me}:", 'adding', $n, 'for min_unit=Y');
                    $result[-1]->[0] += $n;     # add remainder
                    $mon = $secs = 0;   # force stop
                }
            } else {
                $mon += 12 * $year;
                verbose(7, "${me}:", "#mon=$mon", 'added', $year, 'years');
            }
        }
         if ($mon > 0 && $max_unit >= _SECS_PER_MONTH) {
            push(@result, [$mon, 'M']);
            while ($mon > 0) {
                if ($tm0[3] != 0) {     # mday difference
                    my $days;

                    --$mon;
                    $tm1[4] -= $sign;   # previous month
                    if ($tm1[4] < 0 || $tm1[4] > 11) {
                        $tm1[4] %= 12;
                        $tm1[5] -= $sign;       # one year less
                    }
                    $days = days_in_month(1900 + $tm1[5], $tm1[4]);
                    $secs -= SECS_PER_DAY * $days;
                    verbose(7, "${me}:", "#mon=$mon", "#days=$days",
                            "#secs=$secs", "year=$tm1[5]", "mon=$tm1[4]");
                } else {
                    $secs = $tm0[0] + SECS_PER_MIN * $tm0[1] +
                        SECS_PER_HOUR * $tm0[2];
                    verbose(7, "${me}:", "#secs=$secs", 'from',
                            "$tm0[2]H", "$tm0[1]M", "$tm0[0]S");
                    last;
                }
            }
            if ($min_unit >= _SECS_PER_MONTH) {
                my $n = floor(1000 * $secs / _SECS_PER_MONTH) / 1000;

                verbose(6, "${me}:", 'adding', $n, 'for min_unit=m');
                $result[-1]->[0] += $n; # add remainder
                $secs = 0;      # force stop
            }
        }
        verbose(5, "${me}:", "year=$tm1[5]", "mon=$tm1[4]", "#secs=$secs");
        $need_more = 0
            if ($secs == 0);
        $max_unit = SECS_PER_WEEK
            if ($max_unit > SECS_PER_WEEK);     # skip years and months
        $min_unit = SECS_PER_WEEK
            if ($min_unit > SECS_PER_WEEK);     # skip years and months
    }
    foreach (@{(DURATION_UNITS)}) {
        my ($duration, $unit) = @$_;

        if ($duration <= $max_unit) {
            my $last_unit = ($duration == $min_unit);

            if ($duration == 0) {       # handle special time separator
                push(@result, ['', $unit])
                    if ($need_more);    # only if something will follow
            } elsif ($secs >= $duration || $last_unit) {
                unless ($last_unit) {
                    my $n = floor($secs / $duration);

                    push(@result, [$n, $unit]);
                    $secs = fmod($secs, $duration);
                    $need_more = 0
                        if ($secs == 0);
                } elsif ($secs > 0 || $need_more) {
                    my $n = floor(1000 * $secs / $duration) / 1000;

                    push(@result, [$n, $unit]);
                    $secs = 0;
                    last;
                }
            } # else omit zero units
        }
    }
    verbose(-1, "${me}:",  'failed', $secs, 'for max_unit', $max_unit,
            'and min_unit', $min_unit)
        unless ($#result > 0 && $secs == 0);
    return @result;
}

# duration in ISO 8601 format
sub ISO_duration($;$$$)
{
    my ($secs, $max_unit, $min_unit, $anchor) = @_;
    my @result = duration_components($secs, $max_unit, $min_unit, $anchor);
    my $result = shift(@result) eq '+' ? 'P' : '-P';

    $result .= join('', map { @$_ } @result);
    return $result;
}

my @tests;
BEGIN {
    @tests = (
        [0, 'PT0S'],
        [1, 'PT1S'],
        [-1, '-PT1S'],
        [SECS_PER_MIN, 'PT1M'],
        [SECS_PER_HOUR, 'PT1H'],
        [SECS_PER_DAY, 'P1D'],
        [SECS_PER_WEEK, 'P1W'],
        [SECS_PER_MIN + 1, 'PT1M1S'],
        [SECS_PER_HOUR + SECS_PER_MIN + 1, 'PT1H1M1S'],
        [SECS_PER_DAY + SECS_PER_HOUR + SECS_PER_MIN + 1, 'P1DT1H1M1S'],
        [SECS_PER_WEEK+ SECS_PER_DAY + SECS_PER_HOUR + SECS_PER_MIN + 1,
         'P1W1DT1H1M1S'],
        [SECS_PER_MIN, 'PT60S', 'S'],
        [SECS_PER_HOUR, 'PT60M', 'M'],
        [SECS_PER_HOUR + 1, 'PT60M1S', 'M'],
        [SECS_PER_HOUR + 1, 'PT60.016M', 'M', 'M'],
        [SECS_PER_DAY, 'PT24H', 'H'],
        [SECS_PER_WEEK, 'P7D', 'D'],
        # 946681200 = Sat Jan  1 00:00:00 2000 (CEST)
        [SECS_PER_DAY * 31, 'P1M', undef, undef, 946681200],
        [SECS_PER_DAY * -31, '-P1M', undef, undef, 946681200],
        [SECS_PER_DAY * (365 * 3 + 1), 'P3Y', undef, undef, 946681200],
        [SECS_PER_DAY * (-365 * 3), '-P3Y', undef, undef, 946681200],
        [SECS_PER_DAY * (days_in_month(2000, 0) + days_in_month(2000, 1) +
                         days_in_month(2000, 2) + days_in_month(2000, 3) +
                         days_in_month(2000, 4) + days_in_month(2000, 5) +
                         days_in_month(2000, 6) + days_in_month(2000, 7) +
                         days_in_month(2000, 8) + days_in_month(2000, 9) +
                         days_in_month(2000, 10) + days_in_month(2000, 11) +
                         days_in_month(2001, 0)) +
         SECS_PER_WEEK + SECS_PER_DAY + SECS_PER_HOUR + SECS_PER_MIN + 1,
         'P1Y1M1W1DT1H1M1S', undef, undef, 946681200],
        [34995661, 'P1Y1M1W1DT1H1M1S', 'Y', undef, 946681200],  # as above
        [34995661, 'P13M1W1DT1H1M1S', 'm', undef, 946681200],   # as above
        [34995661, 'P57W6DT1H1M1S', 'W', undef, 946681200],     # as above
        [34995661, 'P405DT1H1M1S', 'D', undef, 946681200],      # as above
        [34995661, 'PT9721H1M1S', 'H', undef, 946681200],       # as above
        [34995661, 'PT583261M1S', 'M', undef, 946681200],       # as above
        [34995661, 'PT34995661S', 'S', undef, 946681200],       # as above
        [34995661, 'P1.106Y', 'Y', 'Y', 946681200],     # as above
        [34995661, 'P1Y1.259M', 'Y', 'm', 946681200],   # as above
        [34995661, 'P1Y1M1.148W', 'Y', 'W', 946681200], # as above
        [34995661, 'P1Y1M1W1.042D', 'Y', 'D', 946681200],       # as above
        [34995661, 'P1Y1M1W1DT1.016H', 'Y', 'H', 946681200],    # as above
        [34995661, 'P1Y1M1W1DT1H1.016M', 'Y', 'M', 946681200],  # as above
        [34995661, 'P1Y1M1W1DT1H1M1S', 'Y', 'S', 946681200],    # as above
        [34995661, 'P57W6DT1.016H', 'W', 'H', 946681200],       # as above
        [-(SECS_PER_DAY * (days_in_month(1999, 11) + days_in_month(1999, 10) +
                           days_in_month(1999, 9) + days_in_month(1999, 8) +
                           days_in_month(1999, 7) + days_in_month(1999, 6) +
                           days_in_month(1999, 5) + days_in_month(1999, 4) +
                           days_in_month(1999, 3) + days_in_month(1999, 2) +
                           days_in_month(1999, 1) + days_in_month(1999, 0) +
                           days_in_month(1998, 11)) +
           SECS_PER_WEEK + SECS_PER_DAY + SECS_PER_HOUR + SECS_PER_MIN + 1),
         '-P1Y1M1W1DT1H1M1S', undef, undef, 946681200],
        # 951865200 = Wed Mar  1 00:00:00 2000 (CEST)
        [SECS_PER_DAY * 31, 'P1MT1H', undef, undef, 951865200], # DST switch!
        # 1234567890 = Sat Feb 14 00:31:30 2009 (CEST)
        [SECS_PER_DAY * 14, 'P2W', undef, undef, 1234567890],
        [SECS_PER_DAY * 28, 'P1M', undef, undef, 1234567890],
        [SECS_PER_DAY * -28, '-P4W', undef, undef, 1234567890],
        [SECS_PER_DAY * -31, '-P1M', undef, undef, 1234567890],
        [SECS_PER_DAY * -31 - 30, '-P1MT30S', undef, undef, 1234567890],
        [SECS_PER_DAY * -31 - 30, '-P1MT0.5M', undef, 'M', 1234567890],
        [SECS_PER_DAY * 365, 'P1Y', undef, undef, 1234567890],
        [SECS_PER_DAY * 365, 'P12M', 'm', undef, 1234567890],
        [SECS_PER_DAY * -365, '-P11M4W', undef, undef, 1234567890],
        [SECS_PER_DAY * 3650 + SECS_PER_HOUR * 6 + SECS_PER_MIN * 6 + 15,
         'P9Y11M3W5DT6H6M15S', undef, undef, 1234567890],
        [SECS_PER_DAY * 3650 + SECS_PER_HOUR * 6 + SECS_PER_MIN * 6,
         'P9Y11M3W5DT6.1H', undef, 'H', 1234567890],
        [SECS_PER_DAY * 3650 + SECS_PER_HOUR * 6, 'P9Y11M3W5.25D',
         undef, 'D', 1234567890],
        [SECS_PER_DAY * 365 + SECS_PER_HOUR * 6, 'P12M0.25D',
         'm', 'D', 1234567890],
        [SECS_PER_DAY * 364 + SECS_PER_HOUR * 6, 'P52W0.25D',
         'W', 'D', 1234567890],
        [SECS_PER_DAY * 367.5, 'P52.5W', 'W', 'W', 1234567890],
        [-79682500, '-P2Y6M1W3DT6H1M40S', undef, undef, 1721050231],
    );
}

$verbosity = 5;
use Test::More tests => scalar(@tests);
foreach (@tests) {
    my ($in, $out, $max, $min, $anchor) = @$_;

    is(ISO_duration($in, $max, $min, $anchor), $out);
}
done_testing();

在实际应用程序中,您可以从结果数组中创建类似

1 year, 9 months, 2 weeks, and 2.958 days ago
的字符串(对于非英语语言,获得正确的复数和语法可能很棘手,例如“vor 8 Jahren, 4 Monaten, 3 Wochen und 4.47 Tagen”与“vor 8 Jahren, 4 Monaten, 3 Wochen und 4.47 Tagen”对比“...wird 2 Jahre、8 Monate 和 4 Wochen gültig sein”)。

如果您不需要最小和最大持续时间,您可以显着简化算法。

如果您发现任何错误,请随时报告。

© www.soinside.com 2019 - 2024. All rights reserved.