Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions Bugzilla/Util.pm
Original file line number Diff line number Diff line change
Expand Up @@ -649,25 +649,26 @@ sub time_ago {

# DateTime object or seconds
my $ss = ref($param) ? time() - $param->epoch : $param;
my $mm = round($ss / 60);
my $hh = round($ss / (60 * 60));
my $dd = round($ss / (60 * 60 * 24));
# Use floor for intermediate calculations to avoid rounding issues
my $mm = floor($ss / 60);
my $hh = floor($ss / (60 * 60));
my $dd = floor($ss / (60 * 60 * 24));
# They are not the best definition of month and year,
# but they should be good enough to be used here.
my $mo = round($ss / (60 * 60 * 24 * 30));
my $yy = round($ss / (60 * 60 * 24 * 365.2422));
my $mo = floor($ss / (60 * 60 * 24 * 30));
my $yy = floor($ss / (60 * 60 * 24 * 365.2422));

return 'Just now' if $ss < 10;
return $ss . ' seconds ago' if $ss < 45;
return '1 minute ago' if $ss < 90;
return '1 minute ago' if $mm < 2;
return $mm . ' minutes ago' if $mm < 45;
return '1 hour ago' if $mm < 90;
return '1 hour ago' if $hh < 2;
return $hh . ' hours ago' if $hh < 24;
return '1 day ago' if $hh < 36;
return '1 day ago' if $dd < 2;
return $dd . ' days ago' if $dd < 30;
return '1 month ago' if $dd < 45;
return '1 month ago' if $mo < 2;
return $mo . ' months ago' if $mo < 12;
return '1 year ago' if $mo < 18;
return '1 year ago' if $yy < 2;
return $yy . ' years ago';
}

Expand Down Expand Up @@ -976,7 +977,7 @@ sub extract_nicks {

# This routine is used to determine the latest versions for a
# given product from an external system. We need to fail gracefully
# if the external system is down for any reason. If the call fails,
# if the external system is down for any reason. If the call fails,
# then return undefined and let the caller decide what to do.
sub fetch_product_versions {
my ($product) = @_;
Expand Down
53 changes: 27 additions & 26 deletions js/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,34 +339,35 @@ function bz_toggleClass(anElement, aClass) {
anElement?.classList.toggle(aClass);
}

/* Returns a string representation of a duration.
*
* @param ss Duration in seconds
* or
* @param date Date object
/**
* Returns a string representation of a duration.
* @param {number | Date} param Duration in seconds or a Date object.
* @returns {string} Human-readable time ago string.
*/
function timeAgo(param) {
var ss = param.constructor === Date ? Math.round((new Date() - param) / 1000) : param;
var mm = Math.round(ss / 60),
hh = Math.round(ss / (60 * 60)),
dd = Math.round(ss / (60 * 60 * 24)),
// They are not the best definition of month and year,
// but they should be good enough to be used here.
mo = Math.round(ss / (60 * 60 * 24 * 30)),
yy = Math.round(ss / (60 * 60 * 24 * 365.2422));
const timeAgo = (param) => {
const ss = param.constructor === Date ? Math.round((new Date() - param) / 1000) : param;
// Use Math.floor for intermediate calculations to avoid rounding issues
const mm = Math.floor(ss / 60);
const hh = Math.floor(ss / (60 * 60));
const dd = Math.floor(ss / (60 * 60 * 24));
// They are not the best definition of month and year,
// but they should be good enough to be used here.
const mo = Math.floor(ss / (60 * 60 * 24 * 30));
const yy = Math.floor(ss / (60 * 60 * 24 * 365.2422));

if (ss < 10) return 'Just now';
if (ss < 45) return ss + ' seconds ago';
if (ss < 90) return '1 minute ago';
if (mm < 45) return mm + ' minutes ago';
if (mm < 90) return '1 hour ago';
if (hh < 24) return hh + ' hours ago';
if (hh < 36) return '1 day ago';
if (dd < 30) return dd + ' days ago';
if (dd < 45) return '1 month ago';
if (mo < 12) return mo + ' months ago';
if (mo < 18) return '1 year ago';
return yy + ' years ago';
}
if (ss < 45) return `${ss} seconds ago`;
if (mm < 2) return '1 minute ago';
if (mm < 45) return `${mm} minutes ago`;
if (hh < 2) return '1 hour ago';
if (hh < 24) return `${hh} hours ago`;
if (dd < 2) return '1 day ago';
if (dd < 30) return `${dd} days ago`;
if (mo < 2) return '1 month ago';
if (mo < 12) return `${mo} months ago`;
if (yy < 2) return '1 year ago';
return `${yy} years ago`;
};

/**
* Format the given date as Bugzilla’s standard date format.
Expand Down
55 changes: 54 additions & 1 deletion t/007util.t
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use warnings;

use lib qw(. lib local/lib/perl5 t);
use Support::Files;
use Test::More tests => 20;
use Test::More tests => 52;
use DateTime;

BEGIN {
Expand Down Expand Up @@ -114,3 +114,56 @@ my ($removed, $added) = diff_arrays(\@old_array, \@new_array);
is_deeply($removed, [qw(gamma alpha gamma)],
'diff_array(\@old, \@new) (check removal)');
is_deeply($added, [qw(delta)], 'diff_array(\@old, \@new) (check addition)');

# time_ago():
# Test with seconds
is(time_ago(5), 'Just now', 'time_ago(5) returns "Just now"');
is(time_ago(9), 'Just now', 'time_ago(9) returns "Just now"');
is(time_ago(10), '10 seconds ago', 'time_ago(10) returns "10 seconds ago"');
is(time_ago(30), '30 seconds ago', 'time_ago(30) returns "30 seconds ago"');
is(time_ago(44), '44 seconds ago', 'time_ago(44) returns "44 seconds ago"');

# Test minute boundaries
is(time_ago(45), '1 minute ago', 'time_ago(45) returns "1 minute ago"');
is(time_ago(60), '1 minute ago', 'time_ago(60) returns "1 minute ago"');
is(time_ago(90), '1 minute ago', 'time_ago(90) returns "1 minute ago"');
is(time_ago(119), '1 minute ago', 'time_ago(119) returns "1 minute ago"');
is(time_ago(120), '2 minutes ago', 'time_ago(120) returns "2 minutes ago"');

# Test hour boundaries - critical for the bug fix
is(time_ago(60 * 44), '44 minutes ago', 'time_ago(44 minutes) returns "44 minutes ago"');
is(time_ago(60 * 60), '1 hour ago', 'time_ago(60 minutes) returns "1 hour ago"');
is(time_ago(60 * 90), '1 hour ago', 'time_ago(90 minutes) returns "1 hour ago"');
is(time_ago(60 * 119), '1 hour ago', 'time_ago(119 minutes) returns "1 hour ago"');
is(time_ago(60 * 120), '2 hours ago', 'time_ago(2 hours) returns "2 hours ago"');

# Test day boundaries - this is where the bug was most visible
is(time_ago(60 * 60 * 23), '23 hours ago', 'time_ago(23 hours) returns "23 hours ago"');
is(time_ago(60 * 60 * 24), '1 day ago', 'time_ago(24 hours) returns "1 day ago"');
is(time_ago(60 * 60 * 36), '1 day ago', 'time_ago(36 hours) returns "1 day ago"');
is(time_ago(60 * 60 * 47), '1 day ago', 'time_ago(47 hours) returns "1 day ago"');
is(time_ago(60 * 60 * 48), '2 days ago', 'time_ago(48 hours) returns "2 days ago"');
is(time_ago(60 * 60 * 72), '3 days ago', 'time_ago(72 hours) returns "3 days ago"');

# Test month boundaries
is(time_ago(60 * 60 * 24 * 29), '29 days ago', 'time_ago(29 days) returns "29 days ago"');
is(time_ago(60 * 60 * 24 * 30), '1 month ago', 'time_ago(30 days) returns "1 month ago"');
is(time_ago(60 * 60 * 24 * 45), '1 month ago', 'time_ago(45 days) returns "1 month ago"');
is(time_ago(60 * 60 * 24 * 59), '1 month ago', 'time_ago(59 days) returns "1 month ago"');
is(time_ago(60 * 60 * 24 * 60), '2 months ago', 'time_ago(60 days) returns "2 months ago"');

# Test year boundaries
is(time_ago(60 * 60 * 24 * 365), '1 year ago', 'time_ago(365 days) returns "1 year ago"');
is(time_ago(60 * 60 * 24 * 547), '1 year ago', 'time_ago(547 days) returns "1 year ago"');
is(time_ago(60 * 60 * 24 * 731), '2 years ago', 'time_ago(731 days) returns "2 years ago"');

# Test with DateTime object
my $now = DateTime->now();
my $past = $now->clone->subtract(hours => 25);
is(time_ago($past), '1 day ago', 'time_ago(DateTime 25 hours ago) returns "1 day ago"');

$past = $now->clone->subtract(days => 2);
is(time_ago($past), '2 days ago', 'time_ago(DateTime 2 days ago) returns "2 days ago"');

$past = $now->clone->subtract(months => 1);
like(time_ago($past), qr/^(1 month|2[89]|3[01] days) ago$/, 'time_ago(DateTime 1 month ago) is reasonable');