Simple and efficient formatting of relative date/time using Time::Moment

#!/usr/bin/perl
use strict;
use warnings;

use Carp              qw[];
use Time::Moment 0.19 qw[];

sub YEAR   () { 365.2425    }
sub MONTH  () { YEAR / 12   }
sub DAY    () { 1           }
sub HOUR   () { DAY / 24    }
sub MINUTE () { HOUR / 60   }
sub SECOND () { MINUTE / 60 }

sub ago {
    @_ == 1 or Carp::croak(q/Usage: ago(moment)/);
    my ($moment) = @_;

    my $now = Time::Moment->now;

    ($now->compare($moment) >= 0)
      or Carp::croak(q/Given moment is in the future/);

    my $d = $now->mjd - $moment->mjd;

    if ($d < 0.75 * DAY) {
        if ($d < 0.75 * MINUTE) {
            return 'a few seconds ago';
        }
        elsif ($d < 1.5 * MINUTE) {
            return 'a minute ago';
        }
        elsif ($d < 0.75 * HOUR) {
            return sprintf '%d minutes ago', $d / MINUTE + 0.5;
        }
        elsif ($d < 1.5 * HOUR) {
            return 'an hour ago';
        }
        else {
            return sprintf '%d hours ago', $d / HOUR + 0.5;
        }
    }
    else {
        if ($d < 1.5 * DAY) {
            return 'a day ago';
        }
        elsif ($d < 0.75 * MONTH) {
            return sprintf '%d days ago', $d / DAY + 0.5;
        }
        elsif ($d < 1.5 * MONTH) {
            return 'a month ago';
        }
        elsif ($d < 0.75 * YEAR) {
            return sprintf '%d months ago', $d / MONTH + 0.5;
        }
        elsif ($d < 1.5 * YEAR) {
            return 'a year ago';
        }
        else {
            return sprintf '%d years ago', $d / YEAR + 0.5;
        }
    }
}

my @tests = (
    [ SECOND  *  10, 'a few seconds ago' ],
    [ MINUTE  *   1, 'a minute ago'      ],
    [ SECOND  *  75, 'a minute ago'      ],
    [ MINUTE  *  30, '30 minutes ago'    ],
    [ HOUR    *   1, 'an hour ago'       ],
    [ HOUR    *   2, '2 hours ago'       ],
    [ DAY     *   1, 'a day ago'         ],
    [ DAY     *  20, '20 days ago'       ],
    [ MONTH   *   1, 'a month ago'       ],
    [ MONTH   *   2, '2 months ago'      ],
    [ MONTH   *  13, 'a year ago'        ],
    [ YEAR    *   1, 'a year ago'        ],
    [ YEAR    *   2, '2 years ago'       ],
    [ YEAR    *  10, '10 years ago'      ],
    [ YEAR    * 100, '100 years ago'     ],
);

# Time::Moment 0.25 is required to prove the tests as the 
# ->from_mjd constructor was an addition in 0.25. The ->mjd
# accessor was added in 0.19.

use Time::Moment 0.25;
use Test::More   0.88;

my $now = Time::Moment->now;
foreach my $test (@tests) {
    my ($duration, $expected) = @$test;
    my $tm = Time::Moment->from_mjd($now->mjd - $duration);
    is(ago($tm), $expected, "$tm ($duration)");
}

done_testing();

The above code is also available on gist.github.com.

Before deciding on using the above code, you should review the following code/modules to see if they fits your need better:

https://metacpan.org/pod/Time::Duration
https://metacpan.org/pod/DateTime::Format::Human::Duration
https://metacpan.org/pod/DateTimeX::Format::Ago
https://github.com/mla/time-ago

Leave a comment

About Christian Hansen

user-pic I blog about Perl.