API DateTime.Ago better infers significance of date units.

BUG Fixes missing i18n translation in Date::TimeDiffIn
BUG Fixes Date::TimeDiffIn not respecting mocked SS_Datetime::now
This provides less vague date periods. I.e. "36 days" has a lot more relevance that "1 month"
Reduced duplication of time period calculation code
(ref: CWPBUG-141)
This commit is contained in:
Damian Mooyman 2014-04-11 11:41:38 +12:00
parent c0abb08b61
commit 5b553616dc
3 changed files with 135 additions and 67 deletions

View File

@ -221,9 +221,10 @@ class Date extends DBField {
* Returns the number of seconds/minutes/hours/days or months since the timestamp. * Returns the number of seconds/minutes/hours/days or months since the timestamp.
* *
* @param boolean $includeSeconds Show seconds, or just round to "less than a minute". * @param boolean $includeSeconds Show seconds, or just round to "less than a minute".
* @param int $significance Minimum significant value of X for "X units ago" to display
* @return String * @return String
*/ */
public function Ago($includeSeconds = true) { public function Ago($includeSeconds = true, $significance = 2) {
if($this->value) { if($this->value) {
$time = SS_Datetime::now()->Format('U'); $time = SS_Datetime::now()->Format('U');
if(strtotime($this->value) == $time || $time > strtotime($this->value)) { if(strtotime($this->value) == $time || $time > strtotime($this->value)) {
@ -231,14 +232,14 @@ class Date extends DBField {
'Date.TIMEDIFFAGO', 'Date.TIMEDIFFAGO',
"{difference} ago", "{difference} ago",
'Natural language time difference, e.g. 2 hours ago', 'Natural language time difference, e.g. 2 hours ago',
array('difference' => $this->TimeDiff($includeSeconds)) array('difference' => $this->TimeDiff($includeSeconds, $significance))
); );
} else { } else {
return _t( return _t(
'Date.TIMEDIFFIN', 'Date.TIMEDIFFIN',
"in {difference}", "in {difference}",
'Natural language time difference, e.g. in 2 hours', 'Natural language time difference, e.g. in 2 hours',
array('difference' => $this->TimeDiff($includeSeconds)) array('difference' => $this->TimeDiff($includeSeconds, $significance))
); );
} }
} }
@ -246,79 +247,68 @@ class Date extends DBField {
/** /**
* @param boolean $includeSeconds Show seconds, or just round to "less than a minute". * @param boolean $includeSeconds Show seconds, or just round to "less than a minute".
* @return String * @param int $significance Minimum significant value of X for "X units ago" to display
* @return string
*/ */
public function TimeDiff($includeSeconds = true) { public function TimeDiff($includeSeconds = true, $significance = 2) {
if(!$this->value) return false; if(!$this->value) return false;
$time = SS_Datetime::now()->Format('U'); $time = SS_Datetime::now()->Format('U');
$ago = abs($time - strtotime($this->value)); $ago = abs($time - strtotime($this->value));
if($ago < 60 && !$includeSeconds) {
if($ago < 60 && $includeSeconds) { return _t('Date.LessThanMinuteAgo', 'less than a minute');
$span = $ago; } elseif($ago < $significance * 60 && $includeSeconds) {
$result = ($span != 1) ? "{$span} "._t("Date.SECS", "secs") : "{$span} "._t("Date.SEC", "sec"); return $this->TimeDiffIn('seconds');
} elseif($ago < 60) { } elseif($ago < $significance * 3600) {
$result = _t('Date.LessThanMinuteAgo', 'less than a minute'); return $this->TimeDiffIn('minutes');
} elseif($ago < 3600) { } elseif($ago < $significance * 86400) {
$span = round($ago/60); return $this->TimeDiffIn('hours');
$result = ($span != 1) ? "{$span} "._t("Date.MINS", "mins") : "{$span} "._t("Date.MIN", "min"); } elseif($ago < $significance * 86400 * 30) {
} elseif($ago < 86400) { return $this->TimeDiffIn('days');
$span = round($ago/3600); } elseif($ago < $significance * 86400 * 365) {
$result = ($span != 1) ? "{$span} "._t("Date.HOURS", "hours") : "{$span} "._t("Date.HOUR", "hour"); return $this->TimeDiffIn('months');
} elseif($ago < 86400*30) { } else {
$span = round($ago/86400); return $this->TimeDiffIn('years');
$result = ($span != 1) ? "{$span} "._t("Date.DAYS", "days") : "{$span} "._t("Date.DAY", "day");
} elseif($ago < 86400*365) {
$span = round($ago/86400/30);
$result = ($span != 1) ? "{$span} "._t("Date.MONTHS", "months") : "{$span} "._t("Date.MONTH", "month");
} elseif($ago > 86400*365) {
$span = round($ago/86400/365);
$result = ($span != 1) ? "{$span} "._t("Date.YEARS", "years") : "{$span} "._t("Date.YEAR", "year");
} }
// Replace duplicate spaces, backwards compat with existing translations
$result = preg_replace('/\s+/', ' ', $result);
return $result;
} }
/** /**
* Gets the time difference, but always returns it in a certain format * Gets the time difference, but always returns it in a certain format
* @param string $format The format, could be one of these: *
* @param string $format The format, could be one of these:
* 'seconds', 'minutes', 'hours', 'days', 'months', 'years'. * 'seconds', 'minutes', 'hours', 'days', 'months', 'years'.
* * @return string The resulting formatted period
* @return string
*/ */
public function TimeDiffIn($format) { public function TimeDiffIn($format) {
if($this->value) { if(!$this->value) return false;
$ago = abs(time() - strtotime($this->value));
switch($format) { $time = SS_Datetime::now()->Format('U');
case "seconds": $ago = abs($time - strtotime($this->value));
$span = $ago;
return ($span != 1) ? "{$span} seconds" : "{$span} second"; switch($format) {
break; case "seconds":
case "minutes": $span = $ago;
$span = round($ago/60); return ($span != 1) ? "{$span} "._t("Date.SECS", "secs") : "{$span} "._t("Date.SEC", "sec");
return ($span != 1) ? "{$span} minutes" : "{$span} minute";
break; case "minutes":
case "hours": $span = round($ago/60);
$span = round($ago/3600); return ($span != 1) ? "{$span} "._t("Date.MINS", "mins") : "{$span} "._t("Date.MIN", "min");
return ($span != 1) ? "{$span} hours" : "{$span} hour";
break; case "hours":
case "days": $span = round($ago/3600);
$span = round($ago/86400); return ($span != 1) ? "{$span} "._t("Date.HOURS", "hours") : "{$span} "._t("Date.HOUR", "hour");
return ($span != 1) ? "{$span} days" : "{$span} day";
break; case "days":
case "months": $span = round($ago/86400);
$span = round($ago/86400/30); return ($span != 1) ? "{$span} "._t("Date.DAYS", "days") : "{$span} "._t("Date.DAY", "day");
return ($span != 1) ? "{$span} months" : "{$span} month";
break; case "months":
case "years": $span = round($ago/86400/30);
$span = round($ago/86400/365); return ($span != 1) ? "{$span} "._t("Date.MONTHS", "months") : "{$span} "._t("Date.MONTH", "month");
return ($span != 1) ? "{$span} years" : "{$span} year";
break; case "years":
} $span = round($ago/86400/365);
return ($span != 1) ? "{$span} "._t("Date.YEARS", "years") : "{$span} "._t("Date.YEAR", "year");
} }
} }

View File

@ -164,7 +164,31 @@ class DateTest extends SapphireTest {
SS_Datetime::set_mock_now('2000-12-31 12:00:00'); SS_Datetime::set_mock_now('2000-12-31 12:00:00');
$this->assertEquals( $this->assertEquals(
'10 years ago', '1 month ago',
DBField::create_field('Date', '2000-11-26')->Ago(true, 1),
'Past match on days, less than two months, lowest significance'
);
$this->assertEquals(
'50 days ago', // Rounded from 49.5 days up
DBField::create_field('Date', '2000-11-12')->Ago(),
'Past match on days, less than two months'
);
$this->assertEquals(
'2 months ago',
DBField::create_field('Date', '2000-10-27')->Ago(),
'Past match on days, over two months'
);
$this->assertEquals(
'66 days ago', // rounded from 65.5 days up
DBField::create_field('Date', '2000-10-27')->Ago(true, 3),
'Past match on days, over two months, significance of 3'
);
$this->assertEquals(
'10 years ago',
DBField::create_field('Date', '1990-12-31')->Ago(), DBField::create_field('Date', '1990-12-31')->Ago(),
'Exact past match on years' 'Exact past match on years'
); );
@ -176,7 +200,13 @@ class DateTest extends SapphireTest {
); );
$this->assertEquals( $this->assertEquals(
'1 year ago', '1 year ago',
DBField::create_field('Date', '1999-12-30')->Ago(true, 1),
'Approximate past match in singular, lowest significance'
);
$this->assertEquals(
'12 months ago',
DBField::create_field('Date', '1999-12-30')->Ago(), DBField::create_field('Date', '1999-12-30')->Ago(),
'Approximate past match in singular' 'Approximate past match in singular'
); );
@ -194,7 +224,13 @@ class DateTest extends SapphireTest {
); );
$this->assertEquals( $this->assertEquals(
'in 1 day', 'in 1 day',
DBField::create_field('Date', '2001-01-01')->Ago(true, 1),
'Approximate past match on minutes'
);
$this->assertEquals(
'in 24 hours',
DBField::create_field('Date', '2001-01-01')->Ago(), DBField::create_field('Date', '2001-01-01')->Ago(),
'Approximate past match on minutes' 'Approximate past match on minutes'
); );

View File

@ -105,7 +105,13 @@ class SS_DatetimeTest extends SapphireTest {
); );
$this->assertEquals( $this->assertEquals(
'1 year ago', '1 year ago',
DBField::create_field('SS_Datetime', '1999-12-30 12:00:12')->Ago(true, 1),
'Approximate past match in singular, significance=1'
);
$this->assertEquals(
'12 months ago',
DBField::create_field('SS_Datetime', '1999-12-30 12:00:12')->Ago(), DBField::create_field('SS_Datetime', '1999-12-30 12:00:12')->Ago(),
'Approximate past match in singular' 'Approximate past match in singular'
); );
@ -127,6 +133,36 @@ class SS_DatetimeTest extends SapphireTest {
DBField::create_field('SS_Datetime', '2000-12-31 11:59:01')->Ago(false), DBField::create_field('SS_Datetime', '2000-12-31 11:59:01')->Ago(false),
'Approximate past match on seconds with $includeSeconds=false' 'Approximate past match on seconds with $includeSeconds=false'
); );
$this->assertEquals(
'1 min ago',
DBField::create_field('SS_Datetime', '2000-12-31 11:58:50')->Ago(false),
'Test between 1 and 2 minutes with includeSeconds=false'
);
$this->assertEquals(
'70 secs ago',
DBField::create_field('SS_Datetime', '2000-12-31 11:58:50')->Ago(true),
'Test between 1 and 2 minutes with includeSeconds=true'
);
$this->assertEquals(
'4 mins ago',
DBField::create_field('SS_Datetime', '2000-12-31 11:55:50')->Ago(),
'Past match on minutes'
);
$this->assertEquals(
'1 hour ago',
DBField::create_field('SS_Datetime', '2000-12-31 10:50:58')->Ago(true, 1),
'Past match on hours, significance=1'
);
$this->assertEquals(
'3 hours ago',
DBField::create_field('SS_Datetime', '2000-12-31 08:50:58')->Ago(),
'Past match on hours'
);
SS_Datetime::clear_mock_now(); SS_Datetime::clear_mock_now();
} }
@ -141,7 +177,13 @@ class SS_DatetimeTest extends SapphireTest {
); );
$this->assertEquals( $this->assertEquals(
'in 1 hour', 'in 1 hour',
DBField::create_field('SS_Datetime', '2000-12-31 1:01:05')->Ago(true, 1),
'Approximate past match on minutes, significance=1'
);
$this->assertEquals(
'in 61 mins',
DBField::create_field('SS_Datetime', '2000-12-31 1:01:05')->Ago(), DBField::create_field('SS_Datetime', '2000-12-31 1:01:05')->Ago(),
'Approximate past match on minutes' 'Approximate past match on minutes'
); );