Function for "pretty timestamps" that are human readable and understandable.
authorAndrew Garrett <agarrett@wikimedia.org>
Mon, 16 Jul 2012 23:04:20 +0000 (16:04 -0700)
committerAndrew Garrett <agarrett@wikimedia.org>
Fri, 12 Oct 2012 08:29:15 +0000 (10:29 +0200)
Uses one of the following formats:
- Just now
- 1 minute ago
- 35 minutes ago
- 13:04
- Yesterday at 13:04
- Wednesday at 13:04
- July 16
- July 16, 2012

Change-Id: I53dcf54763c68f15fc4f59b2668001b0cf84adf3

languages/Language.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/language/messages.inc
tests/phpunit/languages/LanguageTest.php

index 0ca21bd..dfa8e2c 100644 (file)
@@ -154,6 +154,10 @@ class Language {
                'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
        );
 
+       // For pretty timestamps
+       // Cutoff for specifying "weekday at XX:XX" format
+       protected $mWeekdayAtCutoff = 432000; // 5 days
+
        /**
         * @since 1.20
         * @var array
@@ -1844,6 +1848,7 @@ class Language {
         * @param $usePrefs Mixed: if true, the user's preference is used
         *                         if false, the site/language default is used
         *                         if int/string, assumed to be a format.
+        *                         if User object, assumed to be a User to get preference from
         * @return string
         */
        function dateFormat( $usePrefs = true ) {
@@ -1855,6 +1860,8 @@ class Language {
                        } else {
                                $datePreference = (string)User::getDefaultOption( 'date' );
                        }
+               } elseif ( $usePrefs instanceof User ) {
+                       $datePreference = $usePrefs->getDatePreference();
                } else {
                        $datePreference = (string)$usePrefs;
                }
@@ -1881,11 +1888,17 @@ class Language {
                                $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
                        } else {
                                $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
+
+                               if ( $type === 'shortdate' && is_null( $df ) ) {
+                                       $df = $this->getDateFormatString( 'date', $pref );
+                               }
+
                                if ( is_null( $df ) ) {
                                        $pref = $this->getDefaultDateFormat();
                                        $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
                                }
                        }
+
                        $this->dateFormatStrings[$type][$pref] = $df;
                }
                return $this->dateFormatStrings[$type][$pref];
@@ -2114,6 +2127,134 @@ class Language {
                return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
        }
 
+       /**
+        * Formats a timestamp in a pretty, human-readable format.
+        * Instead of "13:04, 16 July 2012", we have:
+        * - Just now
+        * - 35 minutes ago
+        * - At 13:04
+        * - Yesterday at 13:04
+        * - Wednesday at 13:04
+        * - July 16, 13:04
+        * - July 16 2012 at 13:04
+        *
+        * @todo Port to JavaScript
+        *
+        * @param $ts Mixed: the time format which needs to be turned into a
+        *            date('YmdHis') format with wfTimestamp(TS_MW,$ts)
+        * @param $relativeTo Mixed: The timestamp to use as "now"
+        * @param $user User: The user to format for (needed for timezone information)
+        * @since 1.20
+        * @return string Formatted timestamp
+        */
+       public function prettyTimestamp( $timestamp, $relativeTo = false, $user ) {
+               // Parameter defaults
+               if ( $relativeTo === false ) {
+                       $relativeTo = wfTimestampNow();
+               }
+
+               // Normalise input to UNIX time
+               $relativeTo = wfTimestamp( TS_UNIX, $relativeTo );
+               $timestamp = wfTimestamp( TS_UNIX, $timestamp );
+               $timeAgo = $relativeTo - $timestamp;
+
+               $adjustedRelativeTo = $this->userAdjust( wfTimestamp( TS_MW, $relativeTo ), $user->getOption('timecorrection') );
+               $adjustedRelativeTo = wfTimestamp( TS_UNIX, $adjustedRelativeTo );
+               $relativeToYear = gmdate( 'Y', $adjustedRelativeTo );
+
+               $adjustedTimestamp = $this->userAdjust( wfTimestamp( TS_MW, $timestamp ), $user->getOption('timecorrection') );
+               $adjustedTimestamp = wfTimestamp( TS_UNIX, $adjustedTimestamp );
+               $timestampYear = gmdate( 'Y', $adjustedTimestamp );
+
+               if ( $timeAgo < 0 ) {
+                       throw new MWException( "Future timestamps not currently supported" );
+               } elseif ( $timeAgo < 30 ) {
+                       return wfMessage( 'just-now' )
+                               ->inLanguage( $this )
+                               ->text();
+               } elseif ( $timeAgo < 5400 ) {
+                       // Less than 90 minutes ago. Return number of hours, minutes or seconds ago.
+                       return $this->formatRelativeTime( $timeAgo );
+               } elseif ( // Same day
+                       intval( $adjustedRelativeTo / (24*60*60) ) ===
+                       intval( $adjustedTimestamp / (24*60*60) )
+               ) {
+                       // Today at XX:XX
+                       $time = $this->time( $adjustedTimestamp );
+                       return wfMessage( 'today-at' )
+                               ->inLanguage( $this )
+                               ->params( $time )
+                               ->text();
+               } elseif ( // Previous day
+                       intval( $adjustedRelativeTo / (24*60*60) ) ===
+                       ( intval( $adjustedTimestamp / (24*60*60) ) + 1 )
+               ) {
+                       // Yesterday at XX:XX
+                       $time = $this->time( $adjustedTimestamp );
+
+                       return wfMessage( 'yesterday-at' )
+                               ->inLanguage( $this )
+                               ->params( $time )
+                               ->text();
+               } elseif ( $timeAgo < ( $this->mWeekdayAtCutoff ) ) { // Less than 5 days ago
+                       // Xday at XX:XX
+                       return $this->formatPastWeekTimestamp( $adjustedTimestamp, $adjustedRelativeTo );
+               } elseif ( $relativeToYear == $timestampYear ) {
+                       // XX XMonth
+                       $df = $this->getDateFormatString( 'shortdate', $this->dateFormat( $user ) );
+                       $mwTimestamp = wfTimestamp( TS_MW, $timestamp );
+                       return $this->sprintfDate( $df, $mwTimestamp );
+               } else {
+                       // Full timestamp
+                       $mwTimestamp = wfTimestamp( TS_MW, $timestamp );
+                       return $this->userDate( $mwTimestamp, $user );
+               }
+       }
+
+       /**
+        * For pretty timestamps: Formats the "X {hours,minutes,seconds} ago" message.
+        *
+        * @param $timeAgo The number of seconds ago the event occurred
+        * @return Formatted string
+        */
+       protected function formatRelativeTime( $timeAgo ) {
+               $count = false;
+               if ( $timeAgo < 60 ) {
+                       $unit = 'seconds';
+                       $count = $timeAgo;
+               } elseif ( $timeAgo < 3600 ) {
+                       $unit = 'minutes';
+                       $count = intval( $timeAgo / 60 );
+               } else {
+                       $unit = 'hours';
+                       $count = intval( $timeAgo / 3600 );
+               }
+
+               return wfMessage( "{$unit}-ago" )->inLanguage( $this )->params( $count )->text();
+       }
+
+       /**
+        * For pretty timestamps: Formats the timestamp for events that occurred
+        * "in the last few days".
+        * (cutoff is configurable by the member variable $mWeekdayAtCutoff)
+        *
+        * @param $eventTimestamp The timestamp of the event, adjusted to
+        *  the user's local timezone, in UNIX format (# of seconds since epoch)
+        * @param $relativeTo The timestamp to format relative to, adjusted to
+        *  the user's local timezone, in UNIX format (# of seconds since epoch)
+        * @return String: The date formatted in a human-friendly way.
+        */
+       protected function formatPastWeekTimestamp( $adjustedTimestamp, $relativeTo ) {
+               $day = date( 'w', $adjustedTimestamp );
+               $weekday = self::$mWeekdayMsgs[$day];
+               $time = $this->time( $adjustedTimestamp );
+
+               return wfMessage( "$weekday-at" )
+                       ->inLanguage( $this )
+                       ->params( $time )
+                       ->text();
+       }
+
        /**
         * @param $key string
         * @return array|null
index f0444cb..4716538 100644 (file)
@@ -162,18 +162,22 @@ $dateFormats = array(
        'mdy time' => 'H:i',
        'mdy date' => 'F j, Y',
        'mdy both' => 'H:i, F j, Y',
+       'mdy shortdate' => 'F j',
 
        'dmy time' => 'H:i',
        'dmy date' => 'j F Y',
        'dmy both' => 'H:i, j F Y',
+       'dmy shortdate' => 'j F',
 
        'ymd time' => 'H:i',
        'ymd date' => 'Y F j',
        'ymd both' => 'H:i, Y F j',
+       'ymd shortdate' => '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 shortdate' => 'xnm-xnd', // This is just confusing
 );
 
 /**
@@ -3857,6 +3861,22 @@ By executing it, your system may be compromised.",
 'days'           => '{{PLURAL:$1|$1 day|$1 days}}',
 'ago'            => '$1 ago',
 
+'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',
+'just-now'       => 'Just now',
+
 # Bad image list
 'bad_image_list' => 'The format is as follows:
 
index c8c958b..3544138 100644 (file)
@@ -3648,6 +3648,21 @@ Part of variable $1 in {{msg-mw|Ago}}
 *{{msg-mw|Hours}}
 *{{msg-mw|Days}}',
 
+'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.',
+'just-now'       => 'Phrase for indicating that something occurred very recently (for example, less than 30 seconds ago)',
+
 # 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.',
 
index 8fa26f6..e4472e4 100644 (file)
@@ -137,6 +137,18 @@ $wgMessageStructure = array(
                'nov',
                'dec',
        ),
+       'pretty-timestamps' => array(
+               'monday-at',
+               'tuesday-at',
+               'wednesday-at',
+               'thursday-at',
+               'friday-at',
+               'saturday-at',
+               'sunday-at',
+               'today-at',
+               'yesterday-at',
+               'just-now',
+       ),
        'categorypages' => array(
                'pagecategories',
                'pagecategorieslink',
index bfb45c7..5ca6a1f 100644 (file)
@@ -1090,5 +1090,128 @@ class LanguageTest extends MediaWikiTestCase {
                        ) ),
                );
        }
-}
 
+       /**
+        * @test
+        * @dataProvider providePrettyTimestampTests
+        */
+       public function testPrettyTimestamp(
+               $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 = new User;
+               $user->setOption( 'timecorrection', $timeCorrection );
+               $user->setOption( 'date', $dateFormat );
+
+               $this->assertEquals(
+                       $expectedOutput,
+                       $this->lang->prettyTimestamp( $tsTime, $currentTime, $user ),
+                       $desc
+               );
+       }
+
+       public function providePrettyTimestampTests() {
+               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',
+                               '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',
+                               '2012-06-17',
+                               'Another month with ISO-8601'
+                       ),
+                       array(
+                               '19910130151500',
+                               '20120716193700',
+                               'Offset|0',
+                               'ISO 8601',
+                               '1991-01-30',
+                               'Different year with ISO-8601',
+                       ),
+               );
+       }
+}