From 35aab8c392ac6ad404bb0d902cca6b50480072da Mon Sep 17 00:00:00 2001 From: Byron Jones Date: Wed, 27 May 2015 11:57:29 +0800 Subject: Bug 1158010: provide a standard and simple way to render relative dates, in perl and javascript --- Bugzilla/Util.pm | 70 +++++++- extensions/BugModal/Config.pm | 8 +- extensions/BugModal/Extension.pm | 7 +- .../template/en/default/bug_modal/header.html.tmpl | 2 +- extensions/BugModal/web/bug_modal.js | 11 ++ extensions/BugModal/web/time_ago.js | 142 ---------------- extensions/MozReview/web/js/mozreview.js | 39 +---- extensions/MyDashboard/lib/Queries.pm | 3 +- extensions/MyDashboard/lib/TimeAgo.pm | 179 -------------------- extensions/RequestNagger/Extension.pm | 3 +- extensions/RequestNagger/lib/TimeAgo.pm | 186 --------------------- extensions/UserProfile/Extension.pm | 3 +- extensions/UserProfile/lib/TimeAgo.pm | 179 -------------------- js/util.js | 27 +++ 14 files changed, 114 insertions(+), 745 deletions(-) delete mode 100644 extensions/BugModal/web/time_ago.js delete mode 100644 extensions/MyDashboard/lib/TimeAgo.pm delete mode 100644 extensions/RequestNagger/lib/TimeAgo.pm delete mode 100644 extensions/UserProfile/lib/TimeAgo.pm diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm index 67798d470..f2291a812 100644 --- a/Bugzilla/Util.pm +++ b/Bugzilla/Util.pm @@ -40,12 +40,13 @@ use base qw(Exporter); validate_ip do_ssl_redirect_if_required use_attachbase diff_arrays on_main_db trim wrap_hard wrap_comment find_wrap_point - format_time validate_date validate_time datetime_from + format_time validate_date validate_time datetime_from time_ago file_mod_time is_7bit_clean bz_crypt generate_random_password validate_email_syntax clean_text get_text template_var disable_utf8 - enable_utf8 detect_encoding email_filter); + enable_utf8 detect_encoding email_filter + round); use Bugzilla::Constants; use Bugzilla::RNG qw(irand); @@ -61,6 +62,7 @@ use Scalar::Util qw(tainted blessed); use Text::Wrap; use Encode qw(encode decode resolve_alias); use Encode::Guess; +use POSIX qw(floor ceil); sub trick_taint { require Carp; @@ -603,6 +605,30 @@ sub datetime_from { return $dt; } +sub time_ago { + my ($param) = @_; + # DateTime object or seconds + my $ss = ref($param) ? time() - $param->epoch : $param; + my $mm = round($ss / 60); + my $hh = round($mm / 60); + my $dd = round($hh / 24); + my $mo = round($dd / 30); + my $yy = round($mo / 12); + + return 'just now' if $ss < 10; + return $ss . ' seconds ago' if $ss < 45; + return 'a minute ago' if $ss < 90; + return $mm . ' minutes ago' if $mm < 45; + return 'an hour ago' if $mm < 90; + return $hh . ' hours ago' if $hh < 24; + return 'a day ago' if $hh < 36; + return $dd . ' days ago' if $dd < 30; + return 'a month ago' if $dd < 45; + return $mo . ' months ago' if $mo < 12; + return 'a year ago' if $mo < 18; + return $yy . ' years ago'; +} + sub file_mod_time { my ($filename) = (@_); my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, @@ -850,6 +876,17 @@ sub _guess_iso { return $encoding; } +# From Math::Round +use constant ROUND_HALF => 0.50000000000008; +sub round { + my @res = map { + $_ >= 0 + ? floor($_ + ROUND_HALF) + : ceil($_ - ROUND_HALF); + } @_; + return (wantarray) ? @res : $res[0]; +} + 1; __END__ @@ -1161,8 +1198,15 @@ You can optionally specify a timezone for the returned date. If not specified, defaults to the currently-logged-in user's timezone, or the Bugzilla server's local timezone if there isn't a logged-in user. -=back +=item C, C + +Returns a concise representation of the time passed. eg. "11 months ago". +Accepts either a DateTime object, which is assumed to be in the past, or +seconds. + + +=back =head2 Files @@ -1239,3 +1283,23 @@ if Bugzilla is currently using the shadowdb or not. Used like: } =back + +=head2 Math and Numbers + +=over + +=item C + +Rounds the number(s) to the nearest integer. In scalar context, returns a +single value; in list context, returns a list of values. Numbers that are +halfway between two integers are rounded "to infinity"; i.e., positive values +are rounded up (e.g., 2.5 becomes 3) and negative values down (e.g., -2.5 +becomes -3). + +=begin undocumented + +Lifted directly from Math::Round to avoid a new dependency for trivial code. + +=end undocumented + +=back diff --git a/extensions/BugModal/Config.pm b/extensions/BugModal/Config.pm index b4242753a..a60a99a7a 100644 --- a/extensions/BugModal/Config.pm +++ b/extensions/BugModal/Config.pm @@ -9,13 +9,7 @@ package Bugzilla::Extension::BugModal; use strict; use constant NAME => 'BugModal'; -use constant REQUIRED_MODULES => [ - { - package => 'Time-Duration', - module => 'Time::Duration', - version => 0 - }, -]; +use constant REQUIRED_MODULES => [ ]; use constant OPTIONAL_MODULES => [ ]; __PACKAGE__->NAME; diff --git a/extensions/BugModal/Extension.pm b/extensions/BugModal/Extension.pm index 0bc96e730..f09a53425 100644 --- a/extensions/BugModal/Extension.pm +++ b/extensions/BugModal/Extension.pm @@ -17,10 +17,9 @@ use Bugzilla::Extension::BugModal::MonkeyPatches; use Bugzilla::Extension::BugModal::Util qw(date_str_to_time); use Bugzilla::Constants; use Bugzilla::User::Setting; -use Bugzilla::Util qw(trick_taint datetime_from html_quote); +use Bugzilla::Util qw(trick_taint datetime_from html_quote time_ago); use List::MoreUtils qw(any); use Template::Stash; -use Time::Duration qw(ago); our $VERSION = '1'; @@ -55,7 +54,7 @@ sub template_after_create { my ($self, $args) = @_; my $context = $args->{template}->context; - # wrapper around Time::Duration::ago() + # wrapper around time_ago() $context->define_filter( time_duration => sub { my ($context) = @_; @@ -63,7 +62,7 @@ sub template_after_create { my ($timestamp) = @_; my $datetime = datetime_from($timestamp) // return $timestamp; - return ago(abs(time() - $datetime->epoch)); + return time_ago($datetime); }; }, 1 ); diff --git a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl index 13ec7d567..13c146ed5 100644 --- a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl @@ -49,11 +49,11 @@ # assets javascript_urls.push( "extensions/ProdCompSearch/web/js/prod_comp_search.js", - "extensions/BugModal/web/time_ago.js", "extensions/BugModal/web/bug_modal.js", "extensions/BugModal/web/ZeroClipboard/ZeroClipboard.min.js", "js/field.js", "js/comments.js", + "js/util.js" ); jquery.push( "datetimepicker", diff --git a/extensions/BugModal/web/bug_modal.js b/extensions/BugModal/web/bug_modal.js index 66e214ad6..32949828f 100644 --- a/extensions/BugModal/web/bug_modal.js +++ b/extensions/BugModal/web/bug_modal.js @@ -8,6 +8,17 @@ $(function() { 'use strict'; + // update relative dates + window.setInterval(function() { + var now = Math.floor(new Date().getTime() / 1000); + $('.rel-time').each(function() { + $(this).text(timeAgo(now - $(this).data('time'))); + }); + $('.rel-time-title').each(function() { + $(this).attr('title', timeAgo(now - $(this).data('time'))); + }); + }, 60000); + // all keywords for autocompletion (lazy-loaded on edit) var keywords = []; diff --git a/extensions/BugModal/web/time_ago.js b/extensions/BugModal/web/time_ago.js deleted file mode 100644 index 59f12692a..000000000 --- a/extensions/BugModal/web/time_ago.js +++ /dev/null @@ -1,142 +0,0 @@ -/* - * this is a port of perl's Time::Duration module, which has the following - * license: - * - * Copyright 2006, Sean M. Burke C, all rights reserved. This - * program is free software; you can redistribute it and/or modify it under the - * same terms as Perl itself. - * - * This program is distributed in the hope that it will be useful, but without - * any warranty; without even the implied warranty of merchantability or - * fitness for a particular purpose. - */ - -$(function() { - 'use strict'; - - function separate(seconds) { - // breakdown of seconds into units, starting with the most significant - - var remainder = seconds; - var tmp; - var wheel = []; - - // years - tmp = Math.floor(remainder / (365 * 24 * 60 * 60)); - wheel.push([ 'year', tmp, 1000000000 ]); - remainder -= tmp * (365 * 24 * 60 * 60); - - // days - tmp = Math.floor(remainder / (24 * 60 * 60)); - wheel.push([ 'day', tmp, 365 ]); - remainder -= tmp * (24 * 60 * 60); - - // hours - tmp = Math.floor(remainder / (60 * 60)); - wheel.push([ 'hour', tmp, 24 ]); - remainder -= tmp * (60 * 60); - - // minutes - tmp = Math.floor(remainder / 60); - wheel.push([ 'minute', tmp, 60 ]); - remainder -= tmp * 60; - - // seconds - wheel.push([ 'second', Math.floor(remainder), 60 ]); - return wheel; - } - - function approximate(precision, wheel) { - // now nudge the wheels into an acceptably (im)precise configuration - FIX: do { - // constraints for leaving this block: - // 1) number of nonzero wheels must be <= precision - // 2) no wheels can be improperly expressed (like having "60" for mins) - - var nonzero_count = 0; - var improperly_expressed = -1; - - for (var i = 0; i < wheel.length; i++) { - var tmp = wheel[i]; - if (tmp[1] == 0) { - continue; - } - ++nonzero_count; - if (i == 0) { - // the years wheel is never improper or over any limit; skip - continue; - } - - if (nonzero_count > precision) { - // this is one nonzero wheel too many - - // incr previous wheel if we're big enough - if (tmp[1] >= (tmp[tmp.length - 1] / 2)) { - ++wheel[i - 1][1]; - } - - // reset this and subsequent wheels to 0 - for (var j = i; j < wheel.length; j++) { - wheel[j][1] = 0; - } - - // start over - continue FIX; - - } else if (tmp[1] >= tmp[tmp.length - 1]) { - // it's an improperly expressed wheel (like "60" on the mins wheel) - improperly_expressed = i; - } - } - - if (improperly_expressed != -1) { - // only fix the least-significant improperly expressed wheel (at a time) - ++wheel[improperly_expressed - 1][1]; - wheel[improperly_expressed][1] = 0; - - // start over - continue FIX; - } - - // otherwise there's not too many nonzero wheels, and there's no - //improperly expressed wheels, so fall thru... - } while(0); - - return wheel; - } - - function render(wheel) { - var parts = []; - wheel.forEach(function(element, index) { - if (element[1] > 0) { - parts.push(element[1] + ' ' + element[0]); - if (element[1] != 1) { - parts[parts.length - 1] += 's'; - } - } - }); - - if (parts.length == 0) { - return "just now"; - } - parts[parts.length - 1] += ' ago'; - if (parts.length == 1) { - return parts[0]; - } - if (parts.length == 2) { - return parts[0] + ' and ' + parts[1]; - } - parts[parts.length - 1] = 'and ' + parts[parts.length - 1]; - return parts.join(', '); - } - - window.setInterval(function() { - var now = Math.floor(new Date().getTime() / 1000); - $('.rel-time').each(function() { - $(this).text(render(approximate(2, separate(now - $(this).data('time'))))); - }); - $('.rel-time-title').each(function() { - $(this).attr('title', render(approximate(2, separate(now - $(this).data('time'))))); - }); - }, 60000); -}); diff --git a/extensions/MozReview/web/js/mozreview.js b/extensions/MozReview/web/js/mozreview.js index 2b9575292..e927d366c 100644 --- a/extensions/MozReview/web/js/mozreview.js +++ b/extensions/MozReview/web/js/mozreview.js @@ -8,43 +8,6 @@ var MozReview = {}; -MozReview.formatElapsedTime = function(s, val) { - val = Math.floor(val); - return val + ' ' + s + (val == 1 ? '' : 's') + ' ago'; -}; - -MozReview.elapsedTime = function(d) { - var ms = Date.now() - d; - - var seconds = ms / 1000; - if (seconds < 60) { - return MozReview.formatElapsedTime('second', seconds); - } - - var minutes = seconds / 60; - if (minutes < 60) { - return MozReview.formatElapsedTime('minute', minutes); - } - - var hours = minutes / 60; - if (hours < 24) { - return MozReview.formatElapsedTime('hour', hours); - } - - var days = hours / 24; - if (days < 30) { - return MozReview.formatElapsedTime("day", days); - } - - var months = days / 30; // enh fudge it - if (months < 12) { - return MozReview.formatElapsedTime("month", months); - } - - var years = months / 12; - return MozReview.formatElapsedTime("year", years); -}; - MozReview.getReviewRequest = function() { var hostUrl = $('.mozreview-requests').data('mozreviewUrl'); var tr = $(''); @@ -83,7 +46,7 @@ MozReview.getReviewRequest = function() { td.clone().text(rr.status), td.clone().text(rr.issue_open_count) .addClass('mozreview-open-issues'), - td.clone().text(MozReview.elapsedTime(new Date(rr.last_updated))) + td.clone().text(timeAgo(new Date(rr.last_updated))) ); if (rr.status == "discarded") { diff --git a/extensions/MyDashboard/lib/Queries.pm b/extensions/MyDashboard/lib/Queries.pm index 34c63e07b..199e8eb46 100644 --- a/extensions/MyDashboard/lib/Queries.pm +++ b/extensions/MyDashboard/lib/Queries.pm @@ -15,10 +15,9 @@ use Bugzilla::CGI; use Bugzilla::Search; use Bugzilla::Flag; use Bugzilla::Status qw(is_open_state); -use Bugzilla::Util qw(format_time datetime_from); +use Bugzilla::Util qw(format_time datetime_from time_ago); use Bugzilla::Extension::MyDashboard::Util qw(open_states quoted_open_states); -use Bugzilla::Extension::MyDashboard::TimeAgo qw(time_ago); use DateTime; diff --git a/extensions/MyDashboard/lib/TimeAgo.pm b/extensions/MyDashboard/lib/TimeAgo.pm deleted file mode 100644 index 0206bfebd..000000000 --- a/extensions/MyDashboard/lib/TimeAgo.pm +++ /dev/null @@ -1,179 +0,0 @@ -package Bugzilla::Extension::MyDashboard::TimeAgo; - -use strict; -use utf8; -use DateTime; -use Carp; -use Exporter qw(import); - -use if $ENV{ARCH_64BIT}, 'integer'; - -our @EXPORT_OK = qw(time_ago); - -our $VERSION = '0.06'; - -my @ranges = ( - [ -1, 'in the future' ], - [ 60, 'just now' ], - [ 900, 'a few minutes ago'], # 15*60 - [ 3000, 'less than an hour ago'], # 50*60 - [ 4500, 'about an hour ago'], # 75*60 - [ 7200, 'more than an hour ago'], # 2*60*60 - [ 21600, 'several hours ago'], # 6*60*60 - [ 86400, 'today', sub { # 24*60*60 - my $time = shift; - my $now = shift; - if ( $time->day < $now->day - or $time->month < $now->month - or $time->year < $now->year - ) { - return 'yesterday' - } - if ($time->hour < 5) { - return 'tonight' - } - if ($time->hour < 10) { - return 'this morning' - } - if ($time->hour < 15) { - return 'today' - } - if ($time->hour < 19) { - return 'this afternoon' - } - return 'this evening' - }], - [ 172800, 'yesterday'], # 2*24*60*60 - [ 604800, 'this week'], # 7*24*60*60 - [ 1209600, 'last week'], # 2*7*24*60*60 - [ 2678400, 'this month', sub { # 31*24*60*60 - my $time = shift; - my $now = shift; - if ($time->year == $now->year and $time->month == $now->month) { - return 'this month' - } - return 'last month' - }], - [ 5356800, 'last month'], # 2*31*24*60*60 - [ 24105600, 'several months ago'], # 9*31*24*60*60 - [ 31536000, 'about a year ago'], # 365*24*60*60 - [ 34214400, 'last year'], # (365+31)*24*60*60 - [ 63072000, 'more than a year ago'], # 2*365*24*60*60 - [ 283824000, 'several years ago'], # 9*365*24*60*60 - [ 315360000, 'about a decade ago'], # 10*365*24*60*60 - [ 630720000, 'last decade'], # 20*365*24*60*60 - [ 2838240000, 'several decades ago'], # 90*365*24*60*60 - [ 3153600000, 'about a century ago'], # 100*365*24*60*60 - [ 6307200000, 'last century'], # 200*365*24*60*60 - [ 6622560000, 'more than a century ago'], # 210*365*24*60*60 - [ 28382400000, 'several centuries ago'], # 900*365*24*60*60 - [ 31536000000, 'about a millenium ago'], # 1000*365*24*60*60 - [ 63072000000, 'more than a millenium ago'], # 2000*365*24*60*60 -); - -sub time_ago { - my ($time, $now) = @_; - - if (not defined $time or not $time->isa('DateTime')) { - croak('DateTime::Duration::Fuzzy::time_ago needs a DateTime object as first parameter') - } - if (not defined $now) { - $now = DateTime->now(); - } - if (not $now->isa('DateTime')) { - croak('Invalid second parameter provided to DateTime::Duration::Fuzzy::time_ago; it must be a DateTime object if provided') - } - - my $dur = $now->subtract_datetime_absolute($time)->in_units('seconds'); - - foreach my $range ( @ranges ) { - if ( $dur <= $range->[0] ) { - if ( $range->[2] ) { - return $range->[2]->($time, $now) - } - return $range->[1] - } - } - - return 'millenia ago' -} - -1 - -__END__ - -=head1 NAME - -DateTime::Duration::Fuzzy -- express dates as fuzzy human-friendly strings - -=head1 SYNOPSIS - - use DateTime::Duration::Fuzzy qw(time_ago); - use DateTime; - - my $now = DateTime->new( - year => 2010, month => 12, day => 12, - hour => 19, minute => 59, - ); - my $then = DateTime->new( - year => 2010, month => 12, day => 12, - hour => 15, - ); - print time_ago($then, $now); - # outputs 'several hours ago' - - print time_ago($then); - # $now taken from C