Browse Source

Add FrozenDate and related tests.

Using a classNameProvider ensures that the Date and FrozenDate behaviors
don't diverge over time.
Mark Story 10 years ago
parent
commit
d9d9ed4448

+ 11 - 0
src/Database/Type/DateTimeType.php

@@ -221,6 +221,17 @@ class DateTimeType extends Type
     }
 
     /**
+     * Change the preferred class name to the FrozenTime implementation.
+     *
+     * @return $this
+     */
+    public function useImmutable()
+    {
+        static::$dateTimeClass = 'Cake\I18n\FrozenTime';
+        return $this;
+    }
+
+    /**
      * Converts a string into a DateTime object after parseing it using the locale
      * aware parser with the specified format.
      *

+ 11 - 0
src/Database/Type/DateType.php

@@ -34,6 +34,17 @@ class DateType extends DateTimeType
     protected $_format = 'Y-m-d';
 
     /**
+     * Change the preferred class name to the FrozenDate implementation.
+     *
+     * @return $this
+     */
+    public function useImmutable()
+    {
+        static::$dateTimeClass = 'Cake\I18n\FrozenDate';
+        return $this;
+    }
+
+    /**
      * Convert request data into a datetime object.
      *
      * @param mixed $value Request data

+ 137 - 0
src/I18n/FrozenDate.php

@@ -0,0 +1,137 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * 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
+ * @since         3.2.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\I18n;
+
+use Cake\Chronos\Date as ChronosDate;
+use IntlDateFormatter;
+use JsonSerializable;
+
+/**
+ * Extends the Date class provided by Chronos.
+ *
+ * Adds handy methods and locale-aware formatting helpers
+ *
+ * This object provides an immutable variant of Cake\I18n\Date
+ */
+class FrozenDate extends ChronosDate implements JsonSerializable
+{
+    use DateFormatTrait;
+
+    /**
+     * The format to use when formatting a time using `Cake\I18n\Date::i18nFormat()`
+     * and `__toString`
+     *
+     * The format should be either the formatting constants from IntlDateFormatter as
+     * described in (http://www.php.net/manual/en/class.intldateformatter.php) or a pattern
+     * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
+     *
+     * It is possible to provide an array of 2 constants. In this case, the first position
+     * will be used for formatting the date part of the object and the second position
+     * will be used to format the time part.
+     *
+     * @var string|array|int
+     * @see \Cake\I18n\DateFormatTrait::i18nFormat()
+     */
+    protected static $_toStringFormat = [IntlDateFormatter::SHORT, -1];
+
+    /**
+     * The format to use when formatting a time using `Cake\I18n\Date::timeAgoInWords()`
+     * and the difference is more than `Cake\I18n\Date::$wordEnd`
+     *
+     * @var string
+     * @see \Cake\I18n\DateFormatTrait::parseDate()
+     */
+    public static $wordFormat = [IntlDateFormatter::SHORT, -1];
+
+    /**
+     * The format to use when formatting a time using `Cake\I18n\Date::nice()`
+     *
+     * The format should be either the formatting constants from IntlDateFormatter as
+     * described in (http://www.php.net/manual/en/class.intldateformatter.php) or a pattern
+     * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
+     *
+     * It is possible to provide an array of 2 constants. In this case, the first position
+     * will be used for formatting the date part of the object and the second position
+     * will be used to format the time part.
+     *
+     * @var string|array|int
+     * @see \Cake\I18n\DateFormatTrait::nice()
+     */
+    public static $niceFormat = [IntlDateFormatter::MEDIUM, -1];
+
+    /**
+     * The format to use when formatting a time using `Date::timeAgoInWords()`
+     * and the difference is less than `Date::$wordEnd`
+     *
+     * @var array
+     * @see \Cake\I18n\Date::timeAgoInWords()
+     */
+    public static $wordAccuracy = [
+        'year' => "day",
+        'month' => "day",
+        'week' => "day",
+        'day' => "day",
+        'hour' => "day",
+        'minute' => "day",
+        'second' => "day",
+    ];
+
+    /**
+     * The end of relative time telling
+     *
+     * @var string
+     * @see \Cake\I18n\Date::timeAgoInWords()
+     */
+    public static $wordEnd = '+1 month';
+
+    /**
+     * Returns either a relative or a formatted absolute date depending
+     * on the difference between the current date and this object.
+     *
+     * ### Options:
+     *
+     * - `from` => another Date object representing the "now" date
+     * - `format` => a fall back format if the relative time is longer than the duration specified by end
+     * - `accuracy` => Specifies how accurate the date should be described (array)
+     *    - year =>   The format if years > 0   (default "day")
+     *    - month =>  The format if months > 0  (default "day")
+     *    - week =>   The format if weeks > 0   (default "day")
+     *    - day =>    The format if weeks > 0   (default "day")
+     * - `end` => The end of relative date telling
+     * - `relativeString` => The printf compatible string when outputting relative date
+     * - `absoluteString` => The printf compatible string when outputting absolute date
+     * - `timezone` => The user timezone the timestamp should be formatted in.
+     *
+     * Relative dates look something like this:
+     *
+     * - 3 weeks, 4 days ago
+     * - 1 day ago
+     *
+     * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using
+     * `i18nFormat`, see the method for the valid formatting strings.
+     *
+     * The returned string includes 'ago' or 'on' and assumes you'll properly add a word
+     * like 'Posted ' before the function output.
+     *
+     * NOTE: If the difference is one week or more, the lowest level of accuracy is day.
+     *
+     * @param array $options Array of options.
+     * @return string Relative time string.
+     */
+    public function timeAgoInWords(array $options = [])
+    {
+        return (new RelativeTimeFormatter($this))->dateAgoInWords($options);
+    }
+}

+ 94 - 45
tests/TestCase/I18n/DateTest.php

@@ -14,6 +14,7 @@
  */
 namespace Cake\Test\TestCase\I18n;
 
+use Cake\I18n\FrozenDate;
 use Cake\I18n\Date;
 use Cake\TestSuite\TestCase;
 use DateTimeZone;
@@ -50,16 +51,28 @@ class DateTest extends TestCase
     {
         parent::tearDown();
         Date::$defaultLocale = $this->locale;
+        FrozenDate::$defaultLocale = $this->locale;
+    }
+
+    /**
+     * Provider for ensuring that Date and FrozenDate work the same way.
+     *
+     * @return void
+     */
+    public static function classNameProvider()
+    {
+        return ['mutable' => ['Cake\I18n\Date'], 'immutable' => ['Cake\I18n\FrozenDate']];
     }
 
     /**
      * test formatting dates taking in account preferred i18n locale file
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testI18nFormat()
+    public function testI18nFormat($class)
     {
-        $time = new Date('Thu Jan 14 13:59:28 2010');
+        $time = new $class('Thu Jan 14 13:59:28 2010');
         $result = $time->i18nFormat();
         $expected = '1/14/10';
         $this->assertEquals($expected, $result);
@@ -73,7 +86,7 @@ class DateTest extends TestCase
         $expected = '00:00:00';
         $this->assertEquals($expected, $result);
 
-        Date::$defaultLocale = 'fr-FR';
+        $class::$defaultLocale = 'fr-FR';
         $result = $time->i18nFormat(\IntlDateFormatter::FULL);
         $expected = 'jeudi 14 janvier 2010 00:00:00 UTC';
         $this->assertEquals($expected, $result);
@@ -85,22 +98,24 @@ class DateTest extends TestCase
     /**
      * test __toString
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testToString()
+    public function testToString($class)
     {
-        $date = new Date('2015-11-06 11:32:45');
+        $date = new $class('2015-11-06 11:32:45');
         $this->assertEquals('11/6/15', (string)$date);
     }
 
     /**
      * test nice()
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testNice()
+    public function testNice($class)
     {
-        $date = new Date('2015-11-06 11:32:45');
+        $date = new $class('2015-11-06 11:32:45');
 
         $this->assertEquals('Nov 6, 2015', $date->nice());
         $this->assertEquals('Nov 6, 2015', $date->nice(new DateTimeZone('America/New_York')));
@@ -110,41 +125,44 @@ class DateTest extends TestCase
     /**
      * test jsonSerialize()
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testJsonSerialize()
+    public function testJsonSerialize($class)
     {
-        $date = new Date('2015-11-06 11:32:45');
+        $date = new $class('2015-11-06 11:32:45');
         $this->assertEquals('"2015-11-06T00:00:00+0000"', json_encode($date));
     }
 
     /**
      * test parseDate()
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testParseDate()
+    public function testParseDate($class)
     {
-        $date = Date::parseDate('11/6/15');
+        $date = $class::parseDate('11/6/15');
         $this->assertEquals('2015-11-06 00:00:00', $date->format('Y-m-d H:i:s'));
 
-        Date::$defaultLocale = 'fr-FR';
-        $date = Date::parseDate('13 10, 2015');
+        $class::$defaultLocale = 'fr-FR';
+        $date = $class::parseDate('13 10, 2015');
         $this->assertEquals('2015-10-13 00:00:00', $date->format('Y-m-d H:i:s'));
     }
 
     /**
      * test parseDateTime()
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testParseDateTime()
+    public function testParseDateTime($class)
     {
-        $date = Date::parseDate('11/6/15 12:33:12');
+        $date = $class::parseDate('11/6/15 12:33:12');
         $this->assertEquals('2015-11-06 00:00:00', $date->format('Y-m-d H:i:s'));
 
-        Date::$defaultLocale = 'fr-FR';
-        $date = Date::parseDate('13 10, 2015 12:54:12');
+        $class::$defaultLocale = 'fr-FR';
+        $date = $class::parseDate('13 10, 2015 12:54:12');
         $this->assertEquals('2015-10-13 00:00:00', $date->format('Y-m-d H:i:s'));
     }
 
@@ -187,13 +205,27 @@ class DateTest extends TestCase
     }
 
     /**
+     * testTimeAgoInWords with Frozen Date
+     *
+     * @dataProvider timeAgoProvider
+     * @return void
+     */
+    public function testTimeAgoInWordsFrozenDate($input, $expected)
+    {
+        $date = new FrozenDate($input);
+        $result = $date->timeAgoInWords();
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
      * test the timezone option for timeAgoInWords
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testTimeAgoInWordsTimezone()
+    public function testTimeAgoInWordsTimezone($class)
     {
-        $date = new Date('1990-07-31 20:33:00 UTC');
+        $date = new $class('1990-07-31 20:33:00 UTC');
         $result = $date->timeAgoInWords(
             [
                 'timezone' => 'America/Vancouver',
@@ -264,13 +296,27 @@ class DateTest extends TestCase
     }
 
     /**
+     * test the end option for timeAgoInWords
+     *
+     * @dataProvider timeAgoEndProvider
+     * @return void
+     */
+    public function testTimeAgoInWordsEndFrozenDate($input, $expected, $end)
+    {
+        $time = new FrozenDate($input);
+        $result = $time->timeAgoInWords(['end' => $end]);
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
      * test the custom string options for timeAgoInWords
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testTimeAgoInWordsCustomStrings()
+    public function testTimeAgoInWordsCustomStrings($class)
     {
-        $date = new Date('-8 years -4 months -2 weeks -3 days');
+        $date = new $class('-8 years -4 months -2 weeks -3 days');
         $result = $date->timeAgoInWords([
             'relativeString' => 'at least %s ago',
             'accuracy' => ['year' => 'year'],
@@ -279,7 +325,7 @@ class DateTest extends TestCase
         $expected = 'at least 8 years ago';
         $this->assertEquals($expected, $result);
 
-        $date = new Date('+4 months +2 weeks +3 days');
+        $date = new $class('+4 months +2 weeks +3 days');
         $result = $date->timeAgoInWords([
             'absoluteString' => 'exactly on %s',
             'accuracy' => ['year' => 'year'],
@@ -292,11 +338,12 @@ class DateTest extends TestCase
     /**
      * Test the accuracy option for timeAgoInWords()
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testDateAgoInWordsAccuracy()
+    public function testDateAgoInWordsAccuracy($class)
     {
-        $date = new Date('+8 years +4 months +2 weeks +3 days');
+        $date = new $class('+8 years +4 months +2 weeks +3 days');
         $result = $date->timeAgoInWords([
             'accuracy' => ['year' => 'year'],
             'end' => '+10 years'
@@ -304,7 +351,7 @@ class DateTest extends TestCase
         $expected = '8 years';
         $this->assertEquals($expected, $result);
 
-        $date = new Date('+8 years +4 months +2 weeks +3 days');
+        $date = new $class('+8 years +4 months +2 weeks +3 days');
         $result = $date->timeAgoInWords([
             'accuracy' => ['year' => 'month'],
             'end' => '+10 years'
@@ -312,7 +359,7 @@ class DateTest extends TestCase
         $expected = '8 years, 4 months';
         $this->assertEquals($expected, $result);
 
-        $date = new Date('+8 years +4 months +2 weeks +3 days');
+        $date = new $class('+8 years +4 months +2 weeks +3 days');
         $result = $date->timeAgoInWords([
             'accuracy' => ['year' => 'week'],
             'end' => '+10 years'
@@ -320,7 +367,7 @@ class DateTest extends TestCase
         $expected = '8 years, 4 months, 2 weeks';
         $this->assertEquals($expected, $result);
 
-        $date = new Date('+8 years +4 months +2 weeks +3 days');
+        $date = new $class('+8 years +4 months +2 weeks +3 days');
         $result = $date->timeAgoInWords([
             'accuracy' => ['year' => 'day'],
             'end' => '+10 years'
@@ -328,7 +375,7 @@ class DateTest extends TestCase
         $expected = '8 years, 4 months, 2 weeks, 3 days';
         $this->assertEquals($expected, $result);
 
-        $date = new Date('+1 years +5 weeks');
+        $date = new $class('+1 years +5 weeks');
         $result = $date->timeAgoInWords([
             'accuracy' => ['year' => 'year'],
             'end' => '+10 years'
@@ -336,7 +383,7 @@ class DateTest extends TestCase
         $expected = '1 year';
         $this->assertEquals($expected, $result);
 
-        $date = new Date('+23 hours');
+        $date = new $class('+23 hours');
         $result = $date->timeAgoInWords([
             'accuracy' => 'day'
         ]);
@@ -347,23 +394,24 @@ class DateTest extends TestCase
     /**
      * Test the format option of timeAgoInWords()
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testDateAgoInWordsWithFormat()
+    public function testDateAgoInWordsWithFormat($class)
     {
-        $date = new Date('2007-9-25');
+        $date = new $class('2007-9-25');
         $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']);
         $this->assertEquals('on 2007-09-25', $result);
 
-        $date = new Date('2007-9-25');
+        $date = new $class('2007-9-25');
         $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']);
         $this->assertEquals('on 2007-09-25', $result);
 
-        $date = new Date('+2 weeks +2 days');
+        $date = new $class('+2 weeks +2 days');
         $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']);
         $this->assertRegExp('/^2 weeks, [1|2] day(s)?$/', $result);
 
-        $date = new Date('+2 months +2 days');
+        $date = new $class('+2 months +2 days');
         $result = $date->timeAgoInWords(['end' => '1 month', 'format' => 'yyyy-MM-dd']);
         $this->assertEquals('on ' . date('Y-m-d', strtotime('+2 months +2 days')), $result);
     }
@@ -371,53 +419,54 @@ class DateTest extends TestCase
     /**
      * test timeAgoInWords() with negative values.
      *
+     * @dataProvider classNameProvider
      * @return void
      */
-    public function testDateAgoInWordsNegativeValues()
+    public function testDateAgoInWordsNegativeValues($class)
     {
-        $date = new Date('-2 months -2 days');
+        $date = new $class('-2 months -2 days');
         $result = $date->timeAgoInWords(['end' => '3 month']);
         $this->assertEquals('2 months, 2 days ago', $result);
 
-        $date = new Date('-2 months -2 days');
+        $date = new $class('-2 months -2 days');
         $result = $date->timeAgoInWords(['end' => '3 month']);
         $this->assertEquals('2 months, 2 days ago', $result);
 
-        $date = new Date('-2 months -2 days');
+        $date = new $class('-2 months -2 days');
         $result = $date->timeAgoInWords(['end' => '1 month', 'format' => 'yyyy-MM-dd']);
         $this->assertEquals('on ' . date('Y-m-d', strtotime('-2 months -2 days')), $result);
 
-        $date = new Date('-2 years -5 months -2 days');
+        $date = new $class('-2 years -5 months -2 days');
         $result = $date->timeAgoInWords(['end' => '3 years']);
         $this->assertEquals('2 years, 5 months, 2 days ago', $result);
 
-        $date = new Date('-2 weeks -2 days');
+        $date = new $class('-2 weeks -2 days');
         $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']);
         $this->assertEquals('2 weeks, 2 days ago', $result);
 
-        $date = new Date('-3 years -12 months');
+        $date = new $class('-3 years -12 months');
         $result = $date->timeAgoInWords();
         $expected = 'on ' . $date->format('n/j/y');
         $this->assertEquals($expected, $result);
 
-        $date = new Date('-1 month -1 week -6 days');
+        $date = new $class('-1 month -1 week -6 days');
         $result = $date->timeAgoInWords(
             ['end' => '1 year', 'accuracy' => ['month' => 'month']]
         );
         $this->assertEquals('1 month ago', $result);
 
-        $date = new Date('-1 years -2 weeks -3 days');
+        $date = new $class('-1 years -2 weeks -3 days');
         $result = $date->timeAgoInWords(
             ['accuracy' => ['year' => 'year']]
         );
         $expected = 'on ' . $date->format('n/j/y');
         $this->assertEquals($expected, $result);
 
-        $date = new Date('-13 months -5 days');
+        $date = new $class('-13 months -5 days');
         $result = $date->timeAgoInWords(['end' => '2 years']);
         $this->assertEquals('1 year, 1 month, 5 days ago', $result);
 
-        $date = new Date('-23 hours');
+        $date = new $class('-23 hours');
         $result = $date->timeAgoInWords(['accuracy' => 'day']);
         $this->assertEquals('today', $result);
     }