Browse Source

Merge pull request #8935 from cakephp/default-output-timezone-new

Default output timezone v3
José Lorenzo Rodríguez 9 years ago
parent
commit
3c8d0345e5
3 changed files with 216 additions and 7 deletions
  1. 63 2
      src/I18n/DateFormatTrait.php
  2. 6 2
      src/I18n/RelativeTimeFormatter.php
  3. 147 3
      tests/TestCase/I18n/TimeTest.php

+ 63 - 2
src/I18n/DateFormatTrait.php

@@ -16,7 +16,10 @@ namespace Cake\I18n;
 
 use Cake\Chronos\Date as ChronosDate;
 use Cake\Chronos\MutableDate;
+use DateTimeZone;
 use IntlDateFormatter;
+use InvalidArgumentException;
+use RuntimeException;
 
 /**
  * Trait for date formatting methods shared by both Time & Date.
@@ -35,6 +38,14 @@ trait DateFormatTrait
     public static $defaultLocale;
 
     /**
+     * The \DateTimeZone default output timezone used by Time and FrozenTime.
+     *
+     * @var \DateTimeZone|null
+     * @see http://php.net/manual/en/timezones.php
+     */
+    protected static $_defaultOutputTimezone;
+
+    /**
      * In-memory cache of date formatters
      *
      * @var array
@@ -60,11 +71,48 @@ trait DateFormatTrait
     /**
      * Caches whether or not this class is a subclass of a Date or MutableDate
      *
-     * @var boolean
+     * @var bool
      */
     protected static $_isDateInstance;
 
     /**
+     * Gets the default output timezone used by Time and FrozenTime.
+     *
+     * @return \DateTimeZone|null DateTimeZone object in which the date will be displayed or null.
+     * @throws \RuntimeException When being executed on Date/FrozenDate.
+     */
+    public static function getDefaultOutputTimezone()
+    {
+        if (is_subclass_of(static::class, ChronosDate::class) || is_subclass_of(static::class, MutableDate::class)) {
+            throw new RuntimeException('Timezone conversion is not supported by Date/FrozenDate.');
+        }
+        return static::$_defaultOutputTimezone;
+    }
+
+    /**
+     * Sets the default output timezone used by Time and FrozenTime.
+     *
+     * @param string|\DateTimeZone $timezone Timezone string or DateTimeZone object
+     * in which the date will be displayed.
+     * @return void
+     * @throws \RuntimeException When being executed on Date/FrozenDate.
+     * @throws \InvalidArgumentException When $timezone is neither a valid DateTimeZone string nor a \DateTimeZone object.
+     */
+    public static function setDefaultOutputTimezone($timezone)
+    {
+        if (is_subclass_of(static::class, ChronosDate::class) || is_subclass_of(static::class, MutableDate::class)) {
+            throw new RuntimeException('Timezone conversion is not supported by Date/FrozenDate.');
+        }
+        if (is_string($timezone)) {
+            static::$_defaultOutputTimezone = new DateTimeZone($timezone);
+        } elseif ($timezone instanceof DateTimeZone) {
+            static::$_defaultOutputTimezone = $timezone;
+        } else {
+            throw new InvalidArgumentException('Expected valid DateTimeZone string or \DateTimeZone object.');
+        }
+    }
+
+    /**
      * Gets the default locale.
      *
      * @return string|null The default locale string to be used or null.
@@ -157,9 +205,22 @@ trait DateFormatTrait
     {
         $time = $this;
 
+        if ($time instanceof Time || $time instanceof FrozenTime) {
+            $timezone = $timezone ?: static::getDefaultOutputTimezone();
+        }
+        // Detect and prevent null timezone transitions, as datefmt_create will
+        // doubly apply the TZ offset.
+        $currentTimezone = $time->getTimezone();
+        if ($timezone && (
+            (is_string($timezone) && $currentTimezone->getName() === $timezone) ||
+            ($timezone instanceof DateTimeZone && $currentTimezone->getName() === $timezone->getName())
+        )) {
+            $timezone = null;
+        }
+
         if ($timezone) {
             // Handle the immutable and mutable object cases.
-            $time = clone $this;
+            $time = clone $time;
             $time = $time->timezone($timezone);
         }
 

+ 6 - 2
src/I18n/RelativeTimeFormatter.php

@@ -93,8 +93,12 @@ class RelativeTimeFormatter
     public function timeAgoInWords(DatetimeInterface $time, array $options = [])
     {
         $options = $this->_options($options, FrozenTime::class);
+
+        $timezone = null;
         if ($options['timezone']) {
-            $time = $time->timezone($options['timezone']);
+            $timezone = $options['timezone'];
+        } elseif ($time instanceof Time || $time instanceof FrozenTime) {
+            $timezone = $time->getDefaultOutputTimezone();
         }
 
         $now = $options['from']->format('U');
@@ -114,7 +118,7 @@ class RelativeTimeFormatter
         }
 
         if ($diff > abs($now - (new FrozenTime($options['end']))->format('U'))) {
-            return sprintf($options['absoluteString'], $time->i18nFormat($options['format']));
+            return sprintf($options['absoluteString'], $time->i18nFormat($options['format'], $timezone));
         }
 
         $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options);

+ 147 - 3
tests/TestCase/I18n/TimeTest.php

@@ -38,6 +38,10 @@ class TimeTest extends TestCase
         $this->locale = Time::getDefaultLocale();
         Time::setDefaultLocale('en_US');
         FrozenTime::setDefaultLocale('en_US');
+
+        date_default_timezone_set('UTC');
+        Time::setDefaultOutputTimezone('UTC');
+        FrozenTime::setDefaultOutputTimezone('UTC');
     }
 
     /**
@@ -55,8 +59,12 @@ class TimeTest extends TestCase
         FrozenTime::setTestNow($this->frozenNow);
         FrozenTime::setDefaultLocale($this->locale);
         FrozenTime::resetToStringFormat();
-        date_default_timezone_set('UTC');
+
         I18n::locale(I18n::DEFAULT_LOCALE);
+
+        date_default_timezone_set('UTC');
+        Time::setDefaultOutputTimezone('UTC');
+        FrozenTime::setDefaultOutputTimezone('UTC');
     }
 
     /**
@@ -192,6 +200,7 @@ class TimeTest extends TestCase
             ],
         ];
     }
+
     /**
      * test the timezone option for timeAgoInWords
      *
@@ -200,7 +209,37 @@ class TimeTest extends TestCase
      */
     public function testTimeAgoInWordsTimezone($class)
     {
-        $time = new FrozenTime('1990-07-31 20:33:00 UTC');
+        $time = new $class('1990-07-31 20:33:00 UTC');
+        $result = $time->timeAgoInWords(
+            [
+                'timezone' => 'America/Vancouver',
+                'end' => '+1month',
+                'format' => 'dd-MM-YYYY HH:mm:ss'
+            ]
+        );
+        $this->assertEquals('on 31-07-1990 13:33:00', $result);
+    }
+
+    /**
+     * test the timezone option for timeAgoInWords
+     *
+     * @dataProvider classNameProvider
+     * @return void
+     */
+    public function testTimeAgoInWordsTimezoneOutputDefaultTimezone($class)
+    {
+        $class::setDefaultOutputTimezone('Europe/Paris');
+        $time = new $class('1990-07-31 20:33:00 UTC');
+        $result = $time->timeAgoInWords(
+            [
+                'end' => '+1month',
+                'format' => 'dd-MM-YYYY HH:mm:ss'
+            ]
+        );
+        $this->assertEquals('on 31-07-1990 22:33:00', $result);
+
+        $class::setDefaultOutputTimezone('Europe/Berlin');
+        $time = new $class('1990-07-31 20:33:00 UTC');
         $result = $time->timeAgoInWords(
             [
                 'timezone' => 'America/Vancouver',
@@ -422,6 +461,29 @@ class TimeTest extends TestCase
     }
 
     /**
+     * testNiceWithDefaultOutputTimezone method
+     *
+     * @dataProvider classNameProvider
+     * @return void
+     */
+    public function testNiceWithDefaultOutputTimezone($class)
+    {
+        $class::setDefaultOutputTimezone('America/Vancouver');
+        $time = new $class('2014-04-20 20:00', 'UTC');
+
+        $this->assertTimeFormat('Apr 20, 2014, 1:00 PM', $time->nice());
+
+        $result = $time->nice('America/New_York');
+        $this->assertTimeFormat('Apr 20, 2014, 4:00 PM', $result);
+        $this->assertEquals('UTC', $time->getTimezone()->getName());
+
+        $class::setDefaultOutputTimezone('Europe/Paris');
+        $time = new $class('2014-04-20 20:00', 'UTC');
+        $this->assertTimeFormat('20 avr. 2014 22:00', $time->nice(null, 'fr-FR'));
+        $this->assertTimeFormat('20 avr. 2014 16:00', $time->nice('America/New_York', 'fr-FR'));
+    }
+
+    /**
      * test formatting dates taking in account preferred i18n locale file
      *
      * @dataProvider classNameProvider
@@ -482,24 +544,61 @@ class TimeTest extends TestCase
     }
 
     /**
+     * test formatting dates taking in account default output timezones.
+     *
+     * @dataProvider classNameProvider
+     * @return void
+     */
+    public function testI18nFormatWithDefaultOutputTimezone($class)
+    {
+        $time = new $class('Thu Jan 14 13:59:28 2010');
+
+        $class::setDefaultLocale('en-US');
+        $class::setDefaultOutputTimezone('America/Vancouver');
+
+        $result = $time->i18nFormat();
+        $expected = '1/14/10 5:59 AM';
+        $this->assertTimeFormat($expected, $result);
+
+        $result = $time->i18nFormat(null, 'America/Toronto');
+        $expected = '1/14/10 8:59 AM';
+        $this->assertTimeFormat($expected, $result);
+
+
+        $class::setDefaultLocale('de-DE');
+        $class::setDefaultOutputTimezone('Europe/Berlin');
+
+        $result = $time->i18nFormat();
+        $expected = '14.01.10 14:59';
+        $this->assertTimeFormat($expected, $result);
+
+        $result = $time->i18nFormat(null, 'Europe/London');
+        $expected = '14.01.10 13:59';
+        $this->assertTimeFormat($expected, $result);
+    }
+
+    /**
      * test formatting dates with offset style timezone
      *
      * @dataProvider classNameProvider
-     * @see https://github.com/facebook/hhvm/issues/3637
      * @return void
+     * @see https://github.com/facebook/hhvm/issues/3637
      */
     public function testI18nFormatWithOffsetTimezone($class)
     {
+        // Default output format is in UTC
         $time = new $class('2014-01-01T00:00:00+00');
         $result = $time->i18nFormat(\IntlDateFormatter::FULL);
         $expected = 'Wednesday January 1 2014 12:00:00 AM GMT';
         $this->assertTimeFormat($expected, $result);
 
+        $class::setDefaultOutputTimezone('GMT+09:00');
         $time = new $class('2014-01-01T00:00:00+09');
         $result = $time->i18nFormat(\IntlDateFormatter::FULL);
         $expected = 'Wednesday January 1 2014 12:00:00 AM GMT+09:00';
         $this->assertTimeFormat($expected, $result);
 
+        $class::setDefaultOutputTimezone('GMT-01:30');
         $time = new $class('2014-01-01T00:00:00-01:30');
         $result = $time->i18nFormat(\IntlDateFormatter::FULL);
         $expected = 'Wednesday January 1 2014 12:00:00 AM GMT-01:30';
@@ -507,6 +606,34 @@ class TimeTest extends TestCase
     }
 
     /**
+     * test formatting dates with offset style timezone and defaultOutputTimezone
+     *
+     * @dataProvider classNameProvider
+     * @return void
+     * @see https://github.com/facebook/hhvm/issues/3637
+     */
+    public function testI18nFormatWithOffsetTimezoneWithDefaultOutputTimezone($class)
+    {
+        // America/Vancouver is GMT-8 in the winter
+        $class::setDefaultOutputTimezone('America/Vancouver');
+
+        $time = new $class('2014-01-01T00:00:00+00');
+        $result = $time->i18nFormat(\IntlDateFormatter::FULL);
+        $expected = 'Tuesday December 31 2013 4:00:00 PM Pacific Standard Time';
+        $this->assertTimeFormat($expected, $result, 'GMT to GMT-8 should be 8 hours');
+
+        $time = new $class('2014-01-01T00:00:00+09:00');
+        $result = $time->i18nFormat(\IntlDateFormatter::FULL);
+        $expected = 'Tuesday December 31 2013 7:00:00 AM Pacific Standard Time';
+        $this->assertTimeFormat($expected, $result, 'GMT+9 to GMT-8 should be 17hrs');
+
+        $time = new $class('2014-01-01T00:00:00-01:30');
+        $result = $time->i18nFormat(\IntlDateFormatter::FULL);
+        $expected = 'Tuesday December 31 2013 5:30:00 PM Pacific Standard Time';
+        $this->assertTimeFormat($expected, $result, 'GMT-1:30 to GMT-8 is 6.5hrs');
+    }
+
+    /**
      * testListTimezones
      *
      * @dataProvider classNameProvider
@@ -554,6 +681,21 @@ class TimeTest extends TestCase
     }
 
     /**
+     * Tests that __toString uses the i18n formatter and works with OutputTimezones
+     *
+     * @dataProvider classNameProvider
+     * @return void
+     */
+    public function testToStringWithDefaultOutputTimezone($class)
+    {
+        $class::setDefaultOutputTimezone('America/Vancouver');
+        $time = new $class('2014-04-20 22:10 UTC');
+        $class::setDefaultLocale('fr-FR');
+        $class::setToStringFormat(\IntlDateFormatter::FULL);
+        $this->assertTimeFormat('dimanche 20 avril 2014 15:10:00 heure d’été du Pacifique', (string)$time);
+    }
+
+    /**
      * Tests that __toString uses the i18n formatter
      *
      * @dataProvider classNameProvider
@@ -883,10 +1025,12 @@ class TimeTest extends TestCase
     {
         $expected = str_replace([',', '(', ')', ' at', ' م.', ' ه‍.ش.', ' AP', ' AH', ' SAKA', 'à '], '', $expected);
         $expected = str_replace(['  '], ' ', $expected);
+        $expected = str_replace("d’été", 'avancée', $expected);
 
         $result = str_replace([',', '(', ')', ' at', ' م.', ' ه‍.ش.', ' AP', ' AH', ' SAKA', 'à '], '', $result);
         $result = str_replace(['گرینویچ'], 'GMT', $result);
         $result = str_replace(['  '], ' ', $result);
+        $expected = str_replace("d’été", 'avancée', $result);
 
         return $this->assertSame($expected, $result, $message);
     }