Refactor MWTimestamp::getHumanTimestamp and add hook.

Changed logic in MWTimestamp::getHumanTimestamp so that
all the message and formatting was offloaded into the
Language class, keeping only actual timestamp logic in
the MWTimestamp class.

Also added a hook so extensions can override the
human timestamp format.

Change-Id: Ie667088010e24eb6cb569f9e8e8e2553005223eb
This commit is contained in:
Tyler Anthony Romeo 2013-01-24 16:14:21 -05:00 committed by kaldari
parent af125df519
commit 7e3386d417
7 changed files with 377 additions and 58 deletions

View file

@ -1138,6 +1138,15 @@ $title: Title object of page
$url: string value as output (out parameter, can modify)
$query: query options passed to Title::getFullURL()
'GetHumanTimestamp': Pre-emptively override the human-readable timestamp generated
by MWTimestamp::getHumanTimestamp(). Return false in this hook to use the custom
output.
&$output: string for the output timestamp
$timestamp: MWTimestamp object of the current (user-adjusted) timestamp
$relativeTo: MWTimestamp object of the relative (user-adjusted) timestamp
$user: User whose preferences are being used to make timestamp
$lang: Language that will be used to render the timestamp
'GetInternalURL': Modify fully-qualified URLs used for squid cache purging.
$title: Title object of page
$url: string value as output (out parameter, can modify)

View file

@ -45,26 +45,10 @@ class MWTimestamp {
);
/**
* Different units for human readable timestamps.
* @see MWTimestamp::getHumanTimestamp
* The actual timestamp being wrapped (DateTime object).
* @var DateTime
*/
private static $units = array(
"milliseconds" => 1,
"seconds" => 1000, // 1000 milliseconds per second
"minutes" => 60, // 60 seconds per minute
"hours" => 60, // 60 minutes per hour
"days" => 24, // 24 hours per day
"months" => 30, // approximately 30 days per month
"years" => 12, // 12 months per year
);
/**
* The actual timestamp being wrapped. Either a DateTime
* object or a string with a Unix timestamp depending on
* PHP.
* @var string|DateTime
*/
private $timestamp;
public $timestamp;
/**
* Make a new timestamp and set it to the specified time,
@ -168,16 +152,7 @@ class MWTimestamp {
throw new TimestampException( __METHOD__ . ' : Illegal timestamp output type.' );
}
if ( is_object( $this->timestamp ) ) {
// DateTime object was used, call DateTime::format.
$output = $this->timestamp->format( self::$formats[$style] );
} elseif ( TS_UNIX == $style ) {
// Unix timestamp was used and is wanted, just return it.
$output = $this->timestamp;
} else {
// Unix timestamp was used, use gmdate().
$output = gmdate( self::$formats[$style], $this->timestamp );
}
$output = $this->timestamp->format( self::$formats[$style] );
if ( ( $style == TS_RFC2822 ) || ( $style == TS_POSTGRES ) ) {
$output .= ' GMT';
@ -194,31 +169,105 @@ class MWTimestamp {
* largest possible unit is used.
*
* @since 1.20
* @since 1.22 Uses Language::getHumanTimestamp to produce the timestamp
*
* @return Message Formatted timestamp
* @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now)
* @param User|null $user User the timestamp is being generated for (or null to use main context's user)
* @param Language|null $lang Language to use to make the human timestamp (or null to use main context's language)
* @return string Formatted timestamp
*/
public function getHumanTimestamp() {
$then = $this->getTimestamp( TS_UNIX );
$now = time();
$timeago = ($now - $then) * 1000;
$message = false;
public function getHumanTimestamp( MWTimestamp $relativeTo = null, User $user = null, Language $lang = null ) {
if ( $relativeTo === null ) {
$relativeTo = new self();
}
if ( $user === null ) {
$user = RequestContext::getMain()->getUser();
}
if ( $lang === null ) {
$lang = RequestContext::getMain()->getLanguage();
}
foreach ( self::$units as $unit => $factor ) {
$next = $timeago / $factor;
if ( $next < 1 ) {
break;
// Adjust for the user's timezone.
$offsetThis = $this->offsetForUser( $user );
$offsetRel = $relativeTo->offsetForUser( $user );
$ts = '';
if ( wfRunHooks( 'GetHumanTimestamp', array( &$ts, $this, $relativeTo, $user, $lang ) ) ) {
$ts = $lang->getHumanTimestamp( $this, $relativeTo, $user );
}
// Reset the timezone on the objects.
$this->timestamp->sub( $offsetThis );
$relativeTo->timestamp->sub( $offsetRel );
return $ts;
}
/**
* Adjust the timestamp depending on the given user's preferences.
*
* @since 1.22
*
* @param User $user User to take preferences from
* @param[out] MWTimestamp $ts Timestamp to adjust
* @return DateInterval Offset that was applied to the timestamp
*/
public function offsetForUser( User $user ) {
global $wgLocalTZOffset;
$option = $user->getOption( 'timecorrection' );
$data = explode( '|', $option, 3 );
// First handle the case of an actual timezone being specified.
if ( $data[0] == 'ZoneInfo' ) {
try {
$tz = new DateTimeZone( $data[2] );
} catch ( Exception $e ) {
$tz = false;
}
if ( $tz ) {
$this->timestamp->setTimezone( $tz );
return new DateInterval( 'P0Y' );
} else {
$timeago = $next;
$message = array( $unit, floor( $timeago ) );
$data[0] = 'Offset';
}
}
if ( $message ) {
$initial = call_user_func_array( 'wfMessage', $message );
return wfMessage( 'ago', $initial->parse() );
$diff = 0;
// If $option is in fact a pipe-separated value, check the
// first value.
if ( $data[0] == 'System' ) {
// First value is System, so use the system offset.
if ( isset( $wgLocalTZOffset ) ) {
$diff = $wgLocalTZOffset;
}
} elseif ( $data[0] == 'Offset' ) {
// First value is Offset, so use the specified offset
$diff = (int)$data[1];
} else {
return wfMessage( 'just-now' );
// $option actually isn't a pipe separated value, but instead
// a comma separated value. Isn't MediaWiki fun?
$data = explode( ':', $option );
if ( count( $data ) >= 2 ) {
// Combination hours and minutes.
$diff = abs( (int)$data[0] ) * 60 + (int)$data[1];
if ( (int) $data[0] < 0 ) {
$diff *= -1;
}
} else {
// Just hours.
$diff = (int)$data[0] * 60;
}
}
$interval = new DateInterval('PT' . abs( $diff ) . 'M');
if ( $diff < 1 ) {
$interval->invert = 1;
}
$this->timestamp->add( $interval );
return $interval;
}
/**
@ -229,6 +278,17 @@ class MWTimestamp {
public function __toString() {
return $this->getTimestamp();
}
/**
* Calculate the difference between two MWTimestamp objects.
*
* @since 1.22
* @param MWTimestamp $relativeTo Base time to calculate difference from
* @return DateInterval|bool The DateInterval object representing the difference between the two dates or false on failure
*/
public function diff( MWTimestamp $relativeTo ) {
return $this->timestamp->diff( $relativeTo->timestamp );
}
}
/**

View file

@ -1961,6 +1961,8 @@ class Language {
* @param $type string May be date, time or both
* @param $pref string The format name as it appears in Messages*.php
*
* @since 1.22 New type 'pretty' that provides a more readable timestamp format
*
* @return string
*/
function getDateFormatString( $type, $pref ) {
@ -1970,7 +1972,12 @@ class Language {
$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
} else {
$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
if ( is_null( $df ) ) {
if ( $type === 'pretty' && $df === null ) {
$df = $this->getDateFormatString( 'date', $pref );
}
if ( $df === null ) {
$pref = $this->getDefaultDateFormat();
$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
}
@ -2203,6 +2210,79 @@ class Language {
return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
}
/**
* Convert an MWTimestamp into a pretty human-readable timestamp using
* the given user preferences and relative base time.
*
* DO NOT USE THIS FUNCTION DIRECTLY. Instead, call MWTimestamp::getHumanTimestamp
* on your timestamp object, which will then call this function. Calling
* this function directly will cause hooks to be skipped over.
*
* @see MWTimestamp::getHumanTimestamp
* @param MWTimestamp $ts Timestamp to prettify
* @param MWTimestamp $relativeTo Base timestamp
* @param User $user User preferences to use
* @return string Human timestamp
* @since 1.21
*/
public function getHumanTimestamp( MWTimestamp $ts, MWTimestamp $relativeTo, User $user ) {
$diff = $ts->diff( $relativeTo );
$diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) - (int)$relativeTo->timestamp->format( 'w' ) );
$days = $diff->days ?: (int)$diffDay;
if ( $diff->invert || $days > 5 && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' ) ) {
// Timestamps are in different years: use full timestamp
// Also do full timestamp for future dates
/**
* @FIXME Add better handling of future timestamps.
*/
$format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
} elseif ( $days > 5 ) {
// Timestamps are in same year, but more than 5 days ago: show day and month only.
$format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
} elseif ( $days > 1 ) {
// Timestamp within the past week: show the day of the week and time
$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
$weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
$ts = wfMessage( "$weekday-at" )
->inLanguage( $this )
->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
->text();
} elseif ( $days == 1 ) {
// Timestamp was yesterday: say 'yesterday' and the time.
$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
$ts = wfMessage( 'yesterday-at' )
->inLanguage( $this )
->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
->text();
} elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
// Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
$ts = wfMessage( 'today-at' )
->inLanguage( $this )
->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
->text();
// From here on in, the timestamp was soon enough ago so that we can simply say
// XX units ago, e.g., "2 hours ago" or "5 minutes ago"
} elseif ( $diff->h == 1 ) {
// Less than 90 minutes, but more than an hour ago.
$ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
} elseif ( $diff->i >= 1 ) {
// A few minutes ago.
$ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
} elseif ( $diff->s >= 30 ) {
// Less than a minute, but more than 30 sec ago.
$ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
} else {
// Less than 30 seconds ago.
$ts = wfMessage( 'just-now' )->text();
}
return $ts;
}
/**
* @param $key string
* @return array|null

View file

@ -162,18 +162,22 @@ $dateFormats = array(
'mdy time' => 'H:i',
'mdy date' => 'F j, Y',
'mdy both' => 'H:i, F j, Y',
'mdy pretty' => 'F j',
'dmy time' => 'H:i',
'dmy date' => 'j F Y',
'dmy both' => 'H:i, j F Y',
'dmy pretty' => 'j F',
'ymd time' => 'H:i',
'ymd date' => 'Y F j',
'ymd both' => 'H:i, Y F j',
'ymd pretty' => 'F j',
'ISO 8601 time' => 'xnH:xni:xns',
'ISO 8601 date' => 'xnY-xnm-xnd',
'ISO 8601 both' => 'xnY-xnm-xnd"T"xnH:xni:xns',
'ISO 8601 pretty' => 'xnm-xnd'
);
/**
@ -3880,11 +3884,27 @@ By executing it, your system may be compromised.",
'minutes' => '{{PLURAL:$1|$1 minute|$1 minutes}}',
'hours' => '{{PLURAL:$1|$1 hour|$1 hours}}',
'days' => '{{PLURAL:$1|$1 day|$1 days}}',
'weeks' => '{{PLURAL:$1|$1 week|$1 weeks}}',
'months' => '{{PLURAL:$1|$1 month|$1 months}}',
'years' => '{{PLURAL:$1|$1 year|$1 years}}',
'ago' => '$1 ago',
'just-now' => 'just now',
'hours-ago' => '$1 {{PLURAL:$1|hour|hours}} ago',
'minutes-ago' => '$1 {{PLURAL:$1|minute|minutes}} ago',
'seconds-ago' => '$1 {{PLURAL:$1|seconds|seconds}} ago',
'monday-at' => 'Monday at $1',
'tuesday-at' => 'Tuesday at $1',
'wednesday-at' => 'Wednesday at $1',
'thursday-at' => 'Thursday at $1',
'friday-at' => 'Friday at $1',
'saturday-at' => 'Saturday at $1',
'sunday-at' => 'Sunday at $1',
'today-at' => '$1',
'yesterday-at' => 'Yesterday at $1',
# Bad image list
'bad_image_list' => 'The format is as follows:

View file

@ -7019,6 +7019,9 @@ See also {{msg-mw|Days-abbrev}}
Part of variable $1 in {{msg-mw|Ago}}
{{Identical|Day}}',
'weeks' => 'Full word for "weeks". $1 is the number of weeks.
Part of variable $1 in {{msg-mw|Ago}}',
'months' => 'Full word for "months". $1 is the number of months.
Part of variable $1 in {{msg-mw|Ago}}',
@ -7034,6 +7037,20 @@ Part of variable $1 in {{msg-mw|Ago}}',
*{{msg-mw|Years}}',
'just-now' => 'Phrase for indicating something happened just now.',
'hours-ago' => 'Phrase for indicating that something occurred a certain number of hours ago',
'minutes-ago' => 'Phrase for indicating that something occurred a certain number of minutes ago',
'seconds-ago' => 'Phrase for indicating that something occurred a certain number of seconds ago',
'monday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Monday. $1 is the time.',
'tuesday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Tuesday. $1 is the time.',
'wednesday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Wednesday. $1 is the time.',
'thursday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Thursday. $1 is the time.',
'friday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Friday. $1 is the time.',
'saturday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Saturday. $1 is the time.',
'sunday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Sunday. $1 is the time.',
'today-at' => 'Phrase for indicating that something occurred at a particular time today. $1 is the time.',
'yesterday-at' => 'Phrase for indicating that something occurred at a particular time yesterday. $1 is the time.',
# Bad image list
'bad_image_list' => 'This message only appears to guide administrators to add links with the right format. This will not appear anywhere else in MediaWiki.',

View file

@ -136,6 +136,17 @@ $wgMessageStructure = array(
'nov',
'dec',
),
'human-timestamps' => array(
'monday-at',
'tuesday-at',
'wednesday-at',
'thursday-at',
'friday-at',
'saturday-at',
'sunday-at',
'today-at',
'yesterday-at',
),
'categorypages' => array(
'pagecategories',
'pagecategorieslink',
@ -2808,6 +2819,7 @@ $wgMessageStructure = array(
'minutes',
'hours',
'days',
'weeks',
'months',
'years',
'ago',

View file

@ -50,18 +50,6 @@ class TimestampTest extends MediaWikiTestCase {
$timestamp->getTimestamp( 98 );
}
/**
* Test human readable timestamp format.
*/
function testHumanOutput() {
$timestamp = new MWTimestamp( time() - 3600 );
$this->assertEquals( "1 hour ago", $timestamp->getHumanTimestamp()->inLanguage( 'en' )->text() );
$timestamp = new MWTimestamp( time() - 5184000 );
$this->assertEquals( "2 months ago", $timestamp->getHumanTimestamp()->inLanguage( 'en' )->text() );
$timestamp = new MWTimestamp( time() - 31536000 );
$this->assertEquals( "1 year ago", $timestamp->getHumanTimestamp()->inLanguage( 'en' )->text() );
}
/**
* Returns a list of valid timestamps in the format:
* array( type, timestamp_of_type, timestamp_in_MW )
@ -83,4 +71,137 @@ class TimestampTest extends MediaWikiTestCase {
array( TS_UNIX, '-62135596801', '00001231235959' )
);
}
/**
* @test
* @dataProvider provideHumanTimestampTests
*/
public function testHumanTimestamp(
$tsTime, // The timestamp to format
$currentTime, // The time to consider "now"
$timeCorrection, // The time offset to use
$dateFormat, // The date preference to use
$expectedOutput, // The expected output
$desc // Description
) {
$user = $this->getMock( 'User' );
$user->expects( $this->any() )
->method( 'getOption' )
->with( 'timecorrection' )
->will( $this->returnValue( $timeCorrection ) );
$user->expects( $this->any() )
->method( 'getDatePreference' )
->will( $this->returnValue( $dateFormat ) );
$tsTime = new MWTimestamp( $tsTime );
$currentTime = new MWTimestamp( $currentTime );
$this->assertEquals(
$expectedOutput,
$tsTime->getHumanTimestamp( $currentTime, $user ),
$desc
);
}
public static function provideHumanTimestampTests() {
return array(
array(
'20111231170000',
'20120101000000',
'Offset|0',
'mdy',
'Yesterday at 17:00',
'"Yesterday" across years',
),
array(
'20120717190900',
'20120717190929',
'Offset|0',
'mdy',
'just now',
'"Just now"',
),
array(
'20120717190900',
'20120717191530',
'Offset|0',
'mdy',
'6 minutes ago',
'X minutes ago',
),
array(
'20121006173100',
'20121006173200',
'Offset|0',
'mdy',
'1 minute ago',
'"1 minute ago"',
),
array(
'20120617190900',
'20120717190900',
'Offset|0',
'mdy',
'June 17',
'Another month'
),
array(
'19910130151500',
'20120716193700',
'Offset|0',
'mdy',
'15:15, January 30, 1991',
'Different year',
),
array(
'20120101050000',
'20120101080000',
'Offset|-360',
'mdy',
'Yesterday at 23:00',
'"Yesterday" across years with time correction',
),
array(
'20120714184300',
'20120716184300',
'Offset|-420',
'mdy',
'Saturday at 11:43',
'Recent weekday with time correction',
),
array(
'20120714184300',
'20120715040000',
'Offset|-420',
'mdy',
'11:43',
'Today at another time with time correction',
),
array(
'20120617190900',
'20120717190900',
'Offset|0',
'dmy',
'17 June',
'Another month with dmy'
),
array(
'20120617190900',
'20120717190900',
'Offset|0',
'ISO 8601',
'06-17',
'Another month with ISO-8601'
),
array(
'19910130151500',
'20120716193700',
'Offset|0',
'ISO 8601',
'1991-01-30T15:15:00',
'Different year with ISO-8601',
),
);
}
}