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
25 changes: 25 additions & 0 deletions src/ChronosInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,31 @@ public function subSecond($value = 1);
*/
public function subSeconds($value);

/**
* Get the difference in a human readable format in the current locale.
*
* When comparing a value in the past to default now:
* 1 hour ago
* 5 months ago
*
* When comparing a value in the future to default now:
* 1 hour from now
* 5 months from now
*
* When comparing a value in the past to another value:
* 1 hour before
* 5 months before
*
* When comparing a value in the future to another value:
* 1 hour after
* 5 months after
*
* @param \Cake\Chronos\ChronosInterface|null $other The datetime to compare with.
* @param bool $absolute Removes time difference modifiers ago, after, etc
* @return string
*/
public function diffForHumans(ChronosInterface $other = null, $absolute = false);

/**
* Get the difference in years
*
Expand Down
100 changes: 100 additions & 0 deletions src/DifferenceFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @copyright Copyright (c) Brian Nesbitt <brian@nesbot.com>
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Chronos;

use Cake\Chronos\ChronosInterface;
use Cake\Chronos\Translator;

/**
* Handles formatting differences in text.
*
* Provides a swappable component for other libraries to leverage.
* when localizing or customizing the difference output.
*/
class DifferenceFormatter
{
/**
* Constructor.
*
* @param \Cake\Chronos\Translator|null $translate The text translator object.
*/
public function __construct($translate = null)
{
$this->translate = $translate ?: new Translator();
}

/**
* Get the difference in a human readable format.
*
* @param \Cake\Chronos\ChronosInterface $date The datetime to start with.
* @param \Cake\Chronos\ChronosInterface|null $other The datetime to compare against.
* @param bool $absolute removes time difference modifiers ago, after, etc
* @return string The difference between the two days in a human readable format
* @see Cake\Chronos\ChronosInterface::diffForHumans
*/
public function diffForHumans(ChronosInterface $date, ChronosInterface $other = null, $absolute = false)
{
$isNow = $other === null;
if ($isNow) {
$other = $date->now($date->tz);
}
$diffInterval = $date->diff($other);

switch (true) {
case ($diffInterval->y > 0):
$unit = 'year';
$count = $diffInterval->y;
break;
case ($diffInterval->m > 0):
$unit = 'month';
$count = $diffInterval->m;
break;
case ($diffInterval->d > 0):
$unit = 'day';
$count = $diffInterval->d;
if ($count >= ChronosInterface::DAYS_PER_WEEK) {
$unit = 'week';
$count = (int)($count / ChronosInterface::DAYS_PER_WEEK);
}
break;
case ($diffInterval->h > 0):
$unit = 'hour';
$count = $diffInterval->h;
break;
case ($diffInterval->i > 0):
$unit = 'minute';
$count = $diffInterval->i;
break;
default:
$count = $diffInterval->s;
$unit = 'second';
break;
}
if ($count === 0) {
$count = 1;
}
$time = $this->translate->plural($unit, $count, ['count' => $count]);
if ($absolute) {
return $time;
}
$isFuture = $diffInterval->invert === 1;
$transId = $isNow ? ($isFuture ? 'from_now' : 'ago') : ($isFuture ? 'after' : 'before');

// Some langs have special pluralization for past and future tense.
$tryKeyExists = $unit . '_' . $transId;
if ($this->translate->exists($tryKeyExists)) {
$time = $this->translate->plural($tryKeyExists, $count, ['count' => $count]);
}
return $this->translate->singular($transId, ['time' => $time]);
}
}
48 changes: 48 additions & 0 deletions src/Traits/DifferenceTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use Cake\Chronos\ChronosInterface;
use Cake\Chronos\ChronosInterval;
use Cake\Chronos\DifferenceFormatter;
use DatePeriod;
use DateTimeInterface;

Expand All @@ -28,6 +29,8 @@
*/
trait DifferenceTrait
{
protected static $diffFormatter;

/**
* Get the difference in years
*
Expand Down Expand Up @@ -234,4 +237,49 @@ public static function fromNow($datetime)
$timeNow = new static();
return $timeNow->diff($datetime);
}

/**
* Get the difference in a human readable format.
*
* When comparing a value in the past to default now:
* 1 hour ago
* 5 months ago
*
* When comparing a value in the future to default now:
* 1 hour from now
* 5 months from now
*
* When comparing a value in the past to another value:
* 1 hour before
* 5 months before
*
* When comparing a value in the future to another value:
* 1 hour after
* 5 months after
*
* @param \Cake\Chronos\ChronosInterface|null $other The datetime to compare with.
* @param bool $absolute removes time difference modifiers ago, after, etc
* @return string
*/
public function diffForhumans(ChronosInterface $other = null, $absolute = false)
{
return $this->diffFormatter()->diffForHumans($this, $other, $absolute);
}

/**
* Get the difference formatter instance or overwrite the current one.
*
* @param Cake\Chronos\DifferenceFormatter|null $formatter The formatter instance when setting.
* @return Cake\Chronos\DifferenceFormatter The formatter instance.
*/
public function diffFormatter($formatter = null)
{
if ($formatter === null) {
if (static::$diffFormatter === null) {
static::$diffFormatter = new DifferenceFormatter();
}
return static::$diffFormatter;
}
return static::$diffFormatter = $translator;
}
}
90 changes: 90 additions & 0 deletions src/Translator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php
/**
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Chronos;

/**
* Basic english only 'translator' for diffForHumans()
*/
class Translator
{
/**
* Translation strings.
*
* @var array
*/
public static $strings = [
'year' => '1 year',
'year_plural' => '{count} years',
'month' => '1 month',
'month_plural' => '{count} months',
'week' => '1 week',
'week_plural' => '{count} weeks',
'day' => '1 day',
'day_plural' => '{count} days',
'hour' => '1 hour',
'hour_plural' => '{count} hours',
'minute' => '1 minute',
'minute_plural' => '{count} minutes',
'second' => '1 second',
'second_plural' => '{count} seconds',
'ago' => '{time} ago',
'from_now' => '{time} from now',
'after' => '{time} after',
'before' => '{time} before',
];

/**
* Check if a translation key exists.
*
* @param string $key The key to check.
* @return bool Whether or not the key exists.
*/
public function exists($key)
{
return isset(static::$strings[$key]);
}

/**
* Get a plural message.
*
* @param string $key The key to use.
* @param string $count The number of items in the translation.
* @param array $vars Additional context variables.
* @return string The translated message or ''.
*/
public function plural($key, $count, array $vars = [])
{
if ($count == 1) {
return $this->singular($key, $vars);
}
return $this->singular($key . '_plural', ['count' => $count] + $vars);
}

/**
* Get a singular message.
*
* @param string $key The key to use.
* @param array $vars Additional context variables.
* @return string The translated message or ''.
*/
public function singular($key, array $vars = [])
{
if (isset(static::$strings[$key])) {
$varKeys = array_keys($vars);
foreach ($varKeys as $i => $k) {
$varKeys[$i] = '{' . $k . '}';
}
return str_replace($varKeys, $vars, static::$strings[$key]);
}
return '';
}
}
68 changes: 67 additions & 1 deletion tests/DateTime/DiffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
* @link http://cakephp.org CakePHP(tm) Project
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/

namespace Cake\Chronos\Test\DateTime;

use Cake\Chronos\Chronos;
Expand Down Expand Up @@ -756,4 +755,71 @@ public function testFromNow($class)
$result = $interval->format("%y %m %d %H %i %s");
$this->assertEquals($result, '1 0 6 00 0 51');
}

public function diffForHumansProvider()
{
$now = Chronos::now();
return [
[$now, $now->addYears(11), '11 years before'],
[$now, $now->addYears(1), '1 year before'],
[$now, $now->addMonths(11), '11 months before'],
[$now, $now->addMonths(1), '1 month before'],
[$now, $now->addDays(8), '1 week before'],
[$now, $now->addDays(23), '3 weeks before'],
[$now, $now->addDays(1), '1 day before'],
[$now, $now->addDays(6), '6 days before'],
[$now, $now->addHours(1), '1 hour before'],
[$now, $now->addHours(5), '5 hours before'],
[$now, $now->addHours(23), '23 hours before'],
[$now, $now->addMinutes(1), '1 minute before'],
[$now, $now->addMinutes(5), '5 minutes before'],
[$now, $now->addMinutes(59), '59 minutes before'],
[$now, $now->addSeconds(1), '1 second before'],
[$now, $now->addSeconds(5), '5 seconds before'],
[$now, $now->addSeconds(59), '59 seconds before'],

[$now, $now->subYears(11), '11 years after'],
[$now, $now->subYears(1), '1 year after'],
[$now, $now->subMonths(11), '11 months after'],
[$now, $now->subMonths(1), '1 month after'],
[$now, $now->subDays(8), '1 week after'],
[$now, $now->subDays(23), '3 weeks after'],
[$now, $now->subDays(1), '1 day after'],
[$now, $now->subDays(6), '6 days after'],
[$now, $now->subHours(1), '1 hour after'],
[$now, $now->subHours(5), '5 hours after'],
[$now, $now->subHours(23), '23 hours after'],
[$now, $now->subMinutes(1), '1 minute after'],
[$now, $now->subMinutes(5), '5 minutes after'],
[$now, $now->subMinutes(59), '59 minutes after'],
[$now, $now->subSeconds(1), '1 second after'],
[$now, $now->subSeconds(5), '5 seconds after'],
[$now, $now->subSeconds(59), '59 seconds after'],
];
}

/**
* @dataProvider diffForHumansProvider
* @return void
*/
public function testDiffForHumansRelative($now, $date, $expected)
{
$this->assertSame($expected, $now->diffForHumans($date));
}

public function testDiffForHumansWithNow()
{
$this->wrapWithTestNow(function () {
$this->assertSame('1 second ago', Chronos::now()->subSeconds(1)->diffForHumans());
$this->assertSame('1 second from now', Chronos::now()->addSeconds(1)->diffForHumans());
});
}

public function testDiffForHumansWithNowAbsolute()
{
$this->wrapWithTestNow(function () {
$this->assertSame('1 second', Chronos::now()->subSeconds(1)->diffForHumans(null, true));
$this->assertSame('1 second', Chronos::now()->addSeconds(1)->diffForHumans(null, true));
});
}
}