37b353b8f668b1fd4871aa3158058ae47b72c1e4
[yaffs-website] / web / core / tests / Drupal / Tests / Component / Datetime / DateTimePlusTest.php
1 <?php
2
3 namespace Drupal\Tests\Component\Datetime;
4
5 use Drupal\Component\Datetime\DateTimePlus;
6 use PHPUnit\Framework\TestCase;
7
8 /**
9  * @coversDefaultClass \Drupal\Component\Datetime\DateTimePlus
10  * @group Datetime
11  */
12 class DateTimePlusTest extends TestCase {
13
14   /**
15    * Test creating dates from string and array input.
16    *
17    * @param mixed $input
18    *   Input argument for DateTimePlus.
19    * @param string $timezone
20    *   Timezone argument for DateTimePlus.
21    * @param string $expected
22    *   Expected output from DateTimePlus::format().
23    *
24    * @dataProvider providerTestDates
25    */
26   public function testDates($input, $timezone, $expected) {
27     $date = new DateTimePlus($input, $timezone);
28     $value = $date->format('c');
29
30     if (is_array($input)) {
31       $input = var_export($input, TRUE);
32     }
33     $this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $timezone, $expected, $value));
34   }
35
36   /**
37    * Test creating dates from string and array input.
38    *
39    * @param mixed $input
40    *   Input argument for DateTimePlus.
41    * @param string $timezone
42    *   Timezone argument for DateTimePlus.
43    * @param string $expected
44    *   Expected output from DateTimePlus::format().
45    *
46    * @dataProvider providerTestDateArrays
47    */
48   public function testDateArrays($input, $timezone, $expected) {
49     $date = DateTimePlus::createFromArray($input, $timezone);
50     $value = $date->format('c');
51
52     if (is_array($input)) {
53       $input = var_export($input, TRUE);
54     }
55     $this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $timezone, $expected, $value));
56   }
57
58   /**
59    * Test date diffs.
60    *
61    * @param mixed $input1
62    *   A DateTimePlus object.
63    * @param mixed $input2
64    *   Date argument for DateTimePlus::diff method.
65    * @param bool $absolute
66    *   Absolute flag for DateTimePlus::diff method.
67    * @param \DateInterval $expected
68    *   The expected result of the DateTimePlus::diff operation.
69    *
70    * @dataProvider providerTestDateDiff
71    */
72   public function testDateDiff($input1, $input2, $absolute, \DateInterval $expected) {
73     $interval = $input1->diff($input2, $absolute);
74     $this->assertEquals($interval, $expected);
75   }
76
77   /**
78    * Test date diff exception caused by invalid input.
79    *
80    * @param mixed $input1
81    *   A DateTimePlus object.
82    * @param mixed $input2
83    *   Date argument for DateTimePlus::diff method.
84    * @param bool $absolute
85    *   Absolute flag for DateTimePlus::diff method.
86    *
87    * @dataProvider providerTestInvalidDateDiff
88    */
89   public function testInvalidDateDiff($input1, $input2, $absolute) {
90     if (method_exists($this, 'expectException')) {
91       $this->expectException(\BadMethodCallException::class);
92       $this->expectExceptionMessage('Method Drupal\Component\Datetime\DateTimePlus::diff expects parameter 1 to be a \DateTime or \Drupal\Component\Datetime\DateTimePlus object');
93     }
94     else {
95       $this->setExpectedException(\BadMethodCallException::class, 'Method Drupal\Component\Datetime\DateTimePlus::diff expects parameter 1 to be a \DateTime or \Drupal\Component\Datetime\DateTimePlus object');
96     }
97     $interval = $input1->diff($input2, $absolute);
98   }
99
100   /**
101    * Test creating dates from invalid array input.
102    *
103    * @param mixed $input
104    *   Input argument for DateTimePlus.
105    * @param string $timezone
106    *   Timezone argument for DateTimePlus.
107    * @param string $class
108    *   The Exception subclass to expect to be thrown.
109    *
110    * @dataProvider providerTestInvalidDateArrays
111    */
112   public function testInvalidDateArrays($input, $timezone, $class) {
113     if (method_exists($this, 'expectException')) {
114       $this->expectException($class);
115     }
116     else {
117       $this->setExpectedException($class);
118     }
119     $this->assertInstanceOf(
120       '\Drupal\Component\DateTimePlus',
121       DateTimePlus::createFromArray($input, $timezone)
122     );
123   }
124
125   /**
126    * Test creating dates from timestamps, and manipulating timezones.
127    *
128    * @param int $input
129    *   Input argument for DateTimePlus::createFromTimestamp().
130    * @param array $initial
131    *   An array containing:
132    *   - 'timezone_initial' - Timezone argument for DateTimePlus.
133    *   - 'format_initial' - Format argument for DateTimePlus.
134    *   - 'expected_initial_date' - Expected output from DateTimePlus::format().
135    *   - 'expected_initial_timezone' - Expected output from
136    *      DateTimePlus::getTimeZone()::getName().
137    *   - 'expected_initial_offset' - Expected output from DateTimePlus::getOffset().
138    * @param array $transform
139    *   An array containing:
140    *   - 'timezone_transform' - Argument to transform date to another timezone via
141    *     DateTimePlus::setTimezone().
142    *   - 'format_transform' - Format argument to use when transforming date to
143    *     another timezone.
144    *   - 'expected_transform_date' - Expected output from DateTimePlus::format(),
145    *     after timezone transform.
146    *   - 'expected_transform_timezone' - Expected output from
147    *     DateTimePlus::getTimeZone()::getName(), after timezone transform.
148    *   - 'expected_transform_offset' - Expected output from
149    *      DateTimePlus::getOffset(), after timezone transform.
150    *
151    * @dataProvider providerTestTimestamp
152    */
153   public function testTimestamp($input, array $initial, array $transform) {
154     // Initialize a new date object.
155     $date = DateTimePlus::createFromTimestamp($input, $initial['timezone']);
156     $this->assertDateTimestamp($date, $input, $initial, $transform);
157   }
158
159   /**
160    * Test creating dates from datetime strings.
161    *
162    * @param string $input
163    *   Input argument for DateTimePlus().
164    * @param array $initial
165    *   @see testTimestamp()
166    * @param array $transform
167    *   @see testTimestamp()
168    *
169    * @dataProvider providerTestDateTimestamp
170    */
171   public function testDateTimestamp($input, array $initial, array $transform) {
172     // Initialize a new date object.
173     $date = new DateTimePlus($input, $initial['timezone']);
174     $this->assertDateTimestamp($date, $input, $initial, $transform);
175   }
176
177   /**
178    * Assertion helper for testTimestamp and testDateTimestamp since they need
179    * different dataProviders.
180    *
181    * @param \Drupal\Component\Datetime\DateTimePlus $date
182    *   DateTimePlus to test.
183    * @input mixed $input
184    *   The original input passed to the test method.
185    * @param array $initial
186    *   @see testTimestamp()
187    * @param array $transform
188    *   @see testTimestamp()
189    */
190   public function assertDateTimestamp($date, $input, $initial, $transform) {
191     // Check format.
192     $value = $date->format($initial['format']);
193     $this->assertEquals($initial['expected_date'], $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $initial['timezone'], $initial['expected_date'], $value));
194
195     // Check timezone name.
196     $value = $date->getTimeZone()->getName();
197     $this->assertEquals($initial['expected_timezone'], $value, sprintf("The current timezone is %s: should be %s.", $value, $initial['expected_timezone']));
198
199     // Check offset.
200     $value = $date->getOffset();
201     $this->assertEquals($initial['expected_offset'], $value, sprintf("The current offset is %s: should be %s.", $value, $initial['expected_offset']));
202
203     // Transform the date to another timezone.
204     $date->setTimezone(new \DateTimeZone($transform['timezone']));
205
206     // Check transformed format.
207     $value = $date->format($transform['format']);
208     $this->assertEquals($transform['expected_date'], $value, sprintf("Test \$date->setTimezone(new \\DateTimeZone(%s)): should be %s, found %s.", $transform['timezone'], $transform['expected_date'], $value));
209
210     // Check transformed timezone.
211     $value = $date->getTimeZone()->getName();
212     $this->assertEquals($transform['expected_timezone'], $value, sprintf("The current timezone should be %s, found %s.", $transform['expected_timezone'], $value));
213
214     // Check transformed offset.
215     $value = $date->getOffset();
216     $this->assertEquals($transform['expected_offset'], $value, sprintf("The current offset should be %s, found %s.", $transform['expected_offset'], $value));
217   }
218
219   /**
220    * Test creating dates from format strings.
221    *
222    * @param string $input
223    *   Input argument for DateTimePlus.
224    * @param string $timezone
225    *   Timezone argument for DateTimePlus.
226    * @param string $format_date
227    *   Format argument for DateTimePlus::format().
228    * @param string $expected
229    *   Expected output from DateTimePlus::format().
230    *
231    * @dataProvider providerTestDateFormat
232    */
233   public function testDateFormat($input, $timezone, $format, $format_date, $expected) {
234     $date = DateTimePlus::createFromFormat($format, $input, $timezone);
235     $value = $date->format($format_date);
236     $this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s, %s): should be %s, found %s.", $input, $timezone, $format, $expected, $value));
237   }
238
239   /**
240    * Test invalid date handling.
241    *
242    * @param mixed $input
243    *   Input argument for DateTimePlus.
244    * @param string $timezone
245    *   Timezone argument for DateTimePlus.
246    * @param string $format
247    *   Format argument for DateTimePlus.
248    * @param string $message
249    *   Message to print if no errors are thrown by the invalid dates.
250    * @param string $class
251    *   The Exception subclass to expect to be thrown.
252    *
253    * @dataProvider providerTestInvalidDates
254    */
255   public function testInvalidDates($input, $timezone, $format, $message, $class) {
256     if (method_exists($this, 'expectException')) {
257       $this->expectException($class);
258     }
259     else {
260       $this->setExpectedException($class);
261     }
262     DateTimePlus::createFromFormat($format, $input, $timezone);
263   }
264
265   /**
266    * Tests that DrupalDateTime can detect the right timezone to use.
267    * When specified or not.
268    *
269    * @param mixed $input
270    *   Input argument for DateTimePlus.
271    * @param mixed $timezone
272    *   Timezone argument for DateTimePlus.
273    * @param string $expected_timezone
274    *   Expected timezone returned from DateTimePlus::getTimezone::getName().
275    * @param string $message
276    *   Message to print on test failure.
277    *
278    * @dataProvider providerTestDateTimezone
279    */
280   public function testDateTimezone($input, $timezone, $expected_timezone, $message) {
281     $date = new DateTimePlus($input, $timezone);
282     $timezone = $date->getTimezone()->getName();
283     $this->assertEquals($timezone, $expected_timezone, $message);
284   }
285
286   /**
287    * Test that DrupalDateTime can detect the right timezone to use when
288    * constructed from a datetime object.
289    */
290   public function testDateTimezoneWithDateTimeObject() {
291     // Create a date object with another date object.
292     $input = new \DateTime('now', new \DateTimeZone('Pacific/Midway'));
293     $timezone = NULL;
294     $expected_timezone = 'Pacific/Midway';
295     $message = 'DateTimePlus uses the specified timezone if provided.';
296
297     $date = DateTimePlus::createFromDateTime($input, $timezone);
298     $timezone = $date->getTimezone()->getName();
299     $this->assertEquals($timezone, $expected_timezone, $message);
300   }
301
302   /**
303    * Provides data for date tests.
304    *
305    * @return array
306    *   An array of arrays, each containing the input parameters for
307    *   DateTimePlusTest::testDates().
308    *
309    * @see DateTimePlusTest::testDates()
310    */
311   public function providerTestDates() {
312     $dates = [
313       // String input.
314       // Create date object from datetime string.
315       ['2009-03-07 10:30', 'America/Chicago', '2009-03-07T10:30:00-06:00'],
316       // Same during daylight savings time.
317       ['2009-06-07 10:30', 'America/Chicago', '2009-06-07T10:30:00-05:00'],
318       // Create date object from date string.
319       ['2009-03-07', 'America/Chicago', '2009-03-07T00:00:00-06:00'],
320       // Same during daylight savings time.
321       ['2009-06-07', 'America/Chicago', '2009-06-07T00:00:00-05:00'],
322       // Create date object from date string.
323       ['2009-03-07 10:30', 'Australia/Canberra', '2009-03-07T10:30:00+11:00'],
324       // Same during daylight savings time.
325       ['2009-06-07 10:30', 'Australia/Canberra', '2009-06-07T10:30:00+10:00'],
326     ];
327
328     // On 32-bit systems, timestamps are limited to 1901-2038.
329     if (PHP_INT_SIZE > 4) {
330       // Create a date object in the distant past.
331       // @see https://www.drupal.org/node/2795489#comment-12127088
332       if (version_compare(PHP_VERSION, '5.6.15', '>=')) {
333         $dates[] = ['1809-02-12 10:30', 'America/Chicago', '1809-02-12T10:30:00-06:00'];
334       }
335       // Create a date object in the far future.
336       $dates[] = ['2345-01-02 02:04', 'UTC', '2345-01-02T02:04:00+00:00'];
337     }
338
339     return $dates;
340   }
341
342   /**
343    * Provides data for date tests.
344    *
345    * @return array
346    *   An array of arrays, each containing the input parameters for
347    *   DateTimePlusTest::testDates().
348    *
349    * @see DateTimePlusTest::testDates()
350    */
351   public function providerTestDateArrays() {
352     $dates = [
353       // Array input.
354       // Create date object from date array, date only.
355       [['year' => 2010, 'month' => 2, 'day' => 28], 'America/Chicago', '2010-02-28T00:00:00-06:00'],
356       // Create date object from date array with hour.
357       [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], 'America/Chicago', '2010-02-28T10:00:00-06:00'],
358       // Create date object from date array, date only.
359       [['year' => 2010, 'month' => 2, 'day' => 28], 'Europe/Berlin', '2010-02-28T00:00:00+01:00'],
360       // Create date object from date array with hour.
361       [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], 'Europe/Berlin', '2010-02-28T10:00:00+01:00'],
362     ];
363
364     // On 32-bit systems, timestamps are limited to 1901-2038.
365     if (PHP_INT_SIZE > 4) {
366       // Create a date object in the distant past.
367       // @see https://www.drupal.org/node/2795489#comment-12127088
368       if (version_compare(PHP_VERSION, '5.6.15', '>=')) {
369         $dates[] = [['year' => 1809, 'month' => 2, 'day' => 12], 'America/Chicago', '1809-02-12T00:00:00-06:00'];
370       }
371       // Create a date object in the far future.
372       $dates[] = [['year' => 2345, 'month' => 1, 'day' => 2], 'UTC', '2345-01-02T00:00:00+00:00'];
373     }
374
375     return $dates;
376   }
377
378   /**
379    * Provides data for testDateFormats.
380    *
381    * @return array
382    *   An array of arrays, each containing:
383    *   - 'input' - Input to DateTimePlus.
384    *   - 'timezone' - Timezone for DateTimePlus.
385    *   - 'format' - Date format for DateTimePlus.
386    *   - 'format_date' - Date format for use in $date->format() method.
387    *   - 'expected' - The expected return from DateTimePlus.
388    *
389    * @see testDateFormats()
390    */
391   public function providerTestDateFormat() {
392     return [
393       // Create a year-only date.
394       ['2009', NULL, 'Y', 'Y', '2009'],
395       // Create a month and year-only date.
396       ['2009-10', NULL, 'Y-m', 'Y-m', '2009-10'],
397       // Create a time-only date.
398       ['T10:30:00', NULL, '\TH:i:s', 'H:i:s', '10:30:00'],
399       // Create a time-only date.
400       ['10:30:00', NULL, 'H:i:s', 'H:i:s', '10:30:00'],
401     ];
402   }
403
404   /**
405    * Provides data for testInvalidDates.
406    *
407    * @return array
408    *   An array of arrays, each containing:
409    *   - 'input' - Input for DateTimePlus.
410    *   - 'timezone' - Timezone for DateTimePlus.
411    *   - 'format' - Format for DateTimePlus.
412    *   - 'message' - Message to display on failure.
413    *
414    * @see testInvalidDates
415    */
416   public function providerTestInvalidDates() {
417     return [
418       // Test for invalid month names when we are using a short version
419       // of the month.
420       ['23 abc 2012', NULL, 'd M Y', "23 abc 2012 contains an invalid month name and did not produce errors.", \InvalidArgumentException::class],
421       // Test for invalid hour.
422       ['0000-00-00T45:30:00', NULL, 'Y-m-d\TH:i:s', "0000-00-00T45:30:00 contains an invalid hour and did not produce errors.", \UnexpectedValueException::class],
423       // Test for invalid day.
424       ['0000-00-99T05:30:00', NULL, 'Y-m-d\TH:i:s', "0000-00-99T05:30:00 contains an invalid day and did not produce errors.", \UnexpectedValueException::class],
425       // Test for invalid month.
426       ['0000-75-00T15:30:00', NULL, 'Y-m-d\TH:i:s', "0000-75-00T15:30:00 contains an invalid month and did not produce errors.", \UnexpectedValueException::class],
427       // Test for invalid year.
428       ['11-08-01T15:30:00', NULL, 'Y-m-d\TH:i:s', "11-08-01T15:30:00 contains an invalid year and did not produce errors.", \UnexpectedValueException::class],
429
430     ];
431   }
432
433   /**
434    * Data provider for testInvalidDateArrays.
435    *
436    * @return array
437    *   An array of arrays, each containing:
438    *   - 'input' - Input for DateTimePlus.
439    *   - 'timezone' - Timezone for DateTimePlus.
440    *
441    * @see testInvalidDateArrays
442    */
443   public function providerTestInvalidDateArrays() {
444     return [
445       // One year larger than the documented upper limit of checkdate().
446       [['year' => 32768, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
447       // One year smaller than the documented lower limit of checkdate().
448       [['year' => 0, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
449       // Test for invalid month from date array.
450       [['year' => 2010, 'month' => 27, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
451       // Test for invalid hour from date array.
452       [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 80, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
453       // Test for invalid minute from date array.
454       [['year' => 2010, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 88, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
455       // Regression test for https://www.drupal.org/node/2084455.
456       [['hour' => 59, 'minute' => 1, 'second' => 1], 'America/Chicago', \InvalidArgumentException::class],
457     ];
458   }
459
460   /**
461    * Provides data for testDateTimezone.
462    *
463    * @return array
464    *   An array of arrays, each containing:
465    *   - 'date' - Date string or object for DateTimePlus.
466    *   - 'timezone' - Timezone string for DateTimePlus.
467    *   - 'expected' - Expected return from DateTimePlus::getTimezone()::getName().
468    *   - 'message' - Message to display on test failure.
469    *
470    * @see testDateTimezone
471    */
472   public function providerTestDateTimezone() {
473     // Use a common date for most of the tests.
474     $date_string = '2007-01-31 21:00:00';
475
476     // Detect the system timezone.
477     $system_timezone = date_default_timezone_get();
478
479     return [
480       // Create a date object with an unspecified timezone, which should
481       // end up using the system timezone.
482       [$date_string, NULL, $system_timezone, 'DateTimePlus uses the system timezone when there is no site timezone.'],
483       // Create a date object with a specified timezone name.
484       [$date_string, 'America/Yellowknife', 'America/Yellowknife', 'DateTimePlus uses the specified timezone if provided.'],
485       // Create a date object with a timezone object.
486       [$date_string, new \DateTimeZone('Australia/Canberra'), 'Australia/Canberra', 'DateTimePlus uses the specified timezone if provided.'],
487       // Create a date object with another date object.
488       [new DateTimePlus('now', 'Pacific/Midway'), NULL, 'Pacific/Midway', 'DateTimePlus uses the specified timezone if provided.'],
489     ];
490   }
491
492   /**
493    * Provides data for testTimestamp.
494    *
495    * @return array
496    *   An array of arrays, each containing the arguments required for
497    *   self::testTimestamp().
498    *
499    * @see testTimestamp()
500    */
501   public function providerTestTimestamp() {
502     return [
503       // Create date object from a unix timestamp and display it in
504       // local time.
505       [
506         'input' => 0,
507         'initial' => [
508           'timezone' => 'UTC',
509           'format' => 'c',
510           'expected_date' => '1970-01-01T00:00:00+00:00',
511           'expected_timezone' => 'UTC',
512           'expected_offset' => 0,
513         ],
514         'transform' => [
515           'timezone' => 'America/Los_Angeles',
516           'format' => 'c',
517           'expected_date' => '1969-12-31T16:00:00-08:00',
518           'expected_timezone' => 'America/Los_Angeles',
519           'expected_offset' => '-28800',
520         ],
521       ],
522       // Create a date using the timestamp of zero, then display its
523       // value both in UTC and the local timezone.
524       [
525         'input' => 0,
526         'initial' => [
527           'timezone' => 'America/Los_Angeles',
528           'format' => 'c',
529           'expected_date' => '1969-12-31T16:00:00-08:00',
530           'expected_timezone' => 'America/Los_Angeles',
531           'expected_offset' => '-28800',
532         ],
533         'transform' => [
534           'timezone' => 'UTC',
535           'format' => 'c',
536           'expected_date' => '1970-01-01T00:00:00+00:00',
537           'expected_timezone' => 'UTC',
538           'expected_offset' => 0,
539         ],
540       ],
541     ];
542   }
543
544   /**
545    * Provides data for testDateTimestamp.
546    *
547    * @return array
548    *   An array of arrays, each containing the arguments required for
549    *   self::testDateTimestamp().
550    *
551    * @see testDateTimestamp()
552    */
553   public function providerTestDateTimestamp() {
554     return [
555       // Create date object from datetime string in UTC, and convert
556       // it to a local date.
557       [
558         'input' => '1970-01-01 00:00:00',
559         'initial' => [
560           'timezone' => 'UTC',
561           'format' => 'c',
562           'expected_date' => '1970-01-01T00:00:00+00:00',
563           'expected_timezone' => 'UTC',
564           'expected_offset' => 0,
565         ],
566         'transform' => [
567           'timezone' => 'America/Los_Angeles',
568           'format' => 'c',
569           'expected_date' => '1969-12-31T16:00:00-08:00',
570           'expected_timezone' => 'America/Los_Angeles',
571           'expected_offset' => '-28800',
572         ],
573       ],
574       // Convert the local time to UTC using string input.
575       [
576         'input' => '1969-12-31 16:00:00',
577         'initial' => [
578           'timezone' => 'America/Los_Angeles',
579           'format' => 'c',
580           'expected_date' => '1969-12-31T16:00:00-08:00',
581           'expected_timezone' => 'America/Los_Angeles',
582           'expected_offset' => '-28800',
583         ],
584         'transform' => [
585           'timezone' => 'UTC',
586           'format' => 'c',
587           'expected_date' => '1970-01-01T00:00:00+00:00',
588           'expected_timezone' => 'UTC',
589           'expected_offset' => 0,
590         ],
591       ],
592       // Convert the local time to UTC using string input.
593       [
594         'input' => '1969-12-31 16:00:00',
595         'initial' => [
596           'timezone' => 'Europe/Warsaw',
597           'format' => 'c',
598           'expected_date' => '1969-12-31T16:00:00+01:00',
599           'expected_timezone' => 'Europe/Warsaw',
600           'expected_offset' => '+3600',
601         ],
602         'transform' => [
603           'timezone' => 'UTC',
604           'format' => 'c',
605           'expected_date' => '1969-12-31T15:00:00+00:00',
606           'expected_timezone' => 'UTC',
607           'expected_offset' => 0,
608         ],
609       ],
610     ];
611   }
612
613   /**
614    * Provides data for date tests.
615    *
616    * @return array
617    *   An array of arrays, each containing the input parameters for
618    *   DateTimePlusTest::testDateDiff().
619    *
620    * @see DateTimePlusTest::testDateDiff()
621    */
622   public function providerTestDateDiff() {
623
624     $empty_interval = new \DateInterval('PT0S');
625
626     $positive_19_hours = new \DateInterval('PT19H');
627
628     $positive_18_hours = new \DateInterval('PT18H');
629
630     $positive_1_hour = new \DateInterval('PT1H');
631
632     $negative_1_hour = new \DateInterval('PT1H');
633     $negative_1_hour->invert = 1;
634
635     return [
636       // There should be a 19 hour time interval between
637       // new years in Sydney and new years in LA in year 2000.
638       [
639         'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
640         'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
641         'absolute' => FALSE,
642         'expected' => $positive_19_hours,
643       ],
644       // In 1970 Sydney did not observe daylight savings time
645       // So there is only a 18 hour time interval.
646       [
647         'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
648         'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
649         'absolute' => FALSE,
650         'expected' => $positive_18_hours,
651       ],
652       [
653         'input1' => DateTimePlus::createFromFormat('U', 3600, new \DateTimeZone('America/Los_Angeles')),
654         'input2' => DateTimePlus::createFromFormat('U', 0, new \DateTimeZone('UTC')),
655         'absolute' => FALSE,
656         'expected' => $negative_1_hour,
657       ],
658       [
659         'input1' => DateTimePlus::createFromFormat('U', 3600),
660         'input2' => DateTimePlus::createFromFormat('U', 0),
661         'absolute' => FALSE,
662         'expected' => $negative_1_hour,
663       ],
664       [
665         'input1' => DateTimePlus::createFromFormat('U', 3600),
666         'input2' => \DateTime::createFromFormat('U', 0),
667         'absolute' => FALSE,
668         'expected' => $negative_1_hour,
669       ],
670       [
671         'input1' => DateTimePlus::createFromFormat('U', 3600),
672         'input2' => DateTimePlus::createFromFormat('U', 0),
673         'absolute' => TRUE,
674         'expected' => $positive_1_hour,
675       ],
676       [
677         'input1' => DateTimePlus::createFromFormat('U', 3600),
678         'input2' => \DateTime::createFromFormat('U', 0),
679         'absolute' => TRUE,
680         'expected' => $positive_1_hour,
681       ],
682       [
683         'input1' => DateTimePlus::createFromFormat('U', 0),
684         'input2' => DateTimePlus::createFromFormat('U', 0),
685         'absolute' => FALSE,
686         'expected' => $empty_interval,
687       ],
688     ];
689   }
690
691   /**
692    * Provides data for date tests.
693    *
694    * @return array
695    *   An array of arrays, each containing the input parameters for
696    *   DateTimePlusTest::testInvalidDateDiff().
697    *
698    * @see DateTimePlusTest::testInvalidDateDiff()
699    */
700   public function providerTestInvalidDateDiff() {
701     return [
702       [
703         'input1' => DateTimePlus::createFromFormat('U', 3600),
704         'input2' => '1970-01-01 00:00:00',
705         'absolute' => FALSE,
706       ],
707       [
708         'input1' => DateTimePlus::createFromFormat('U', 3600),
709         'input2' => NULL,
710         'absolute' => FALSE,
711       ],
712     ];
713   }
714
715   /**
716    * Tests invalid values passed to constructor.
717    *
718    * @param string $time
719    *   A date/time string.
720    * @param string[] $errors
721    *   An array of error messages.
722    *
723    * @covers ::__construct
724    *
725    * @dataProvider providerTestInvalidConstructor
726    */
727   public function testInvalidConstructor($time, array $errors) {
728     $date = new DateTimePlus($time);
729
730     $this->assertEquals(TRUE, $date->hasErrors());
731     $this->assertEquals($errors, $date->getErrors());
732   }
733
734   /**
735    * Provider for testInvalidConstructor().
736    *
737    * @return array
738    *   An array of invalid date/time strings, and corresponding error messages.
739    */
740   public function providerTestInvalidConstructor() {
741     return [
742       [
743         'YYYY-MM-DD',
744         [
745           'The timezone could not be found in the database',
746           'Unexpected character',
747           'Double timezone specification',
748         ],
749       ],
750       [
751         '2017-MM-DD',
752         [
753           'Unexpected character',
754           'The timezone could not be found in the database',
755         ],
756       ],
757       [
758         'YYYY-03-DD',
759         [
760           'The timezone could not be found in the database',
761           'Unexpected character',
762           'Double timezone specification',
763         ],
764       ],
765       [
766         'YYYY-MM-07',
767         [
768           'The timezone could not be found in the database',
769           'Unexpected character',
770           'Double timezone specification',
771         ],
772       ],
773       [
774         '2017-13-55',
775         [
776           'Unexpected character',
777         ],
778       ],
779       [
780         'YYYY-MM-DD hh:mm:ss',
781         [
782           'The timezone could not be found in the database',
783           'Unexpected character',
784           'Double timezone specification',
785         ],
786       ],
787       [
788         '2017-03-07 25:70:80',
789         [
790           'Unexpected character',
791           'Double time specification',
792         ],
793       ],
794       [
795         'lorem ipsum dolor sit amet',
796         [
797           'The timezone could not be found in the database',
798           'Double timezone specification',
799         ],
800       ],
801     ];
802   }
803
804   /**
805    * Tests the $settings['validate_format'] parameter in ::createFromFormat().
806    */
807   public function testValidateFormat() {
808     // Check that an input that does not strictly follow the input format will
809     // produce the desired date. In this case the year string '11' doesn't
810     // precisely match the 'Y' formatter parameter, but PHP will parse it
811     // regardless. However, when formatted with the same string, the year will
812     // be output with four digits. With the ['validate_format' => FALSE]
813     // $settings, this will not thrown an exception.
814     $date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '11-03-31 17:44:00', 'UTC', ['validate_format' => FALSE]);
815     $this->assertEquals('0011-03-31 17:44:00', $date->format('Y-m-d H:i:s'));
816
817     // Parse the same date with ['validate_format' => TRUE] and make sure we
818     // get the expected exception.
819     if (method_exists($this, 'expectException')) {
820       $this->expectException(\UnexpectedValueException::class);
821     }
822     else {
823       $this->setExpectedException(\UnexpectedValueException::class);
824     }
825     $date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '11-03-31 17:44:00', 'UTC', ['validate_format' => TRUE]);
826   }
827
828   /**
829    * Tests setting the default time for date-only objects.
830    */
831   public function testDefaultDateTime() {
832     $utc = new \DateTimeZone('UTC');
833
834     $date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '2017-05-23 22:58:00', $utc);
835     $this->assertEquals('22:58:00', $date->format('H:i:s'));
836     $date->setDefaultDateTime();
837     $this->assertEquals('12:00:00', $date->format('H:i:s'));
838   }
839
840   /**
841    * Tests that object methods are chainable.
842    *
843    * @covers ::__call
844    */
845   public function testChainable() {
846     $date = new DateTimePlus('now', 'Australia/Sydney');
847
848     $date->setTimestamp(12345678);
849     $rendered = $date->render();
850     $this->assertEquals('1970-05-24 07:21:18 Australia/Sydney', $rendered);
851
852     $date->setTimestamp(23456789);
853     $rendered = $date->setTimezone(new \DateTimeZone('America/New_York'))->render();
854     $this->assertEquals('1970-09-29 07:46:29 America/New_York', $rendered);
855
856     $date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-05-24 07:21:18', new \DateTimeZone('Australia/Sydney'))
857       ->setTimezone(new \DateTimeZone('America/New_York'));
858     $rendered = $date->render();
859     $this->assertInstanceOf(DateTimePlus::class, $date);
860     $this->assertEquals(12345678, $date->getTimestamp());
861     $this->assertEquals('1970-05-23 17:21:18 America/New_York', $rendered);
862   }
863
864   /**
865    * Tests that non-chainable methods work.
866    *
867    * @covers ::__call
868    */
869   public function testChainableNonChainable() {
870     $datetime1 = new DateTimePlus('2009-10-11 12:00:00');
871     $datetime2 = new DateTimePlus('2009-10-13 12:00:00');
872     $interval = $datetime1->diff($datetime2);
873     $this->assertInstanceOf(\DateInterval::class, $interval);
874     $this->assertEquals('+2 days', $interval->format('%R%a days'));
875   }
876
877   /**
878    * Tests that chained calls to non-existent functions throw an exception.
879    *
880    * @covers ::__call
881    */
882   public function testChainableNonCallable() {
883     if (method_exists($this, 'expectException')) {
884       $this->expectException(\BadMethodCallException::class);
885       $this->expectExceptionMessage('Call to undefined method Drupal\Component\Datetime\DateTimePlus::nonexistent()');
886     }
887     else {
888       $this->setExpectedException(\BadMethodCallException::class, 'Call to undefined method Drupal\Component\Datetime\DateTimePlus::nonexistent()');
889     }
890     $date = new DateTimePlus('now', 'Australia/Sydney');
891     $date->setTimezone(new \DateTimeZone('America/New_York'))->nonexistent();
892   }
893
894   /**
895    * @covers ::getPhpDateTime
896    */
897   public function testGetPhpDateTime() {
898     $new_york = new \DateTimeZone('America/New_York');
899     $berlin = new \DateTimeZone('Europe/Berlin');
900
901     // Test retrieving a cloned copy of the wrapped \DateTime object, and that
902     // altering it does not change the DateTimePlus object.
903     $datetimeplus = DateTimePlus::createFromFormat('Y-m-d H:i:s', '2017-07-13 22:40:00', $new_york, ['langcode' => 'en']);
904     $this->assertEquals(1500000000, $datetimeplus->getTimestamp());
905     $this->assertEquals('America/New_York', $datetimeplus->getTimezone()->getName());
906
907     $datetime = $datetimeplus->getPhpDateTime();
908     $this->assertInstanceOf('DateTime', $datetime);
909     $this->assertEquals(1500000000, $datetime->getTimestamp());
910     $this->assertEquals('America/New_York', $datetime->getTimezone()->getName());
911
912     $datetime->setTimestamp(1400000000)->setTimezone($berlin);
913     $this->assertEquals(1400000000, $datetime->getTimestamp());
914     $this->assertEquals('Europe/Berlin', $datetime->getTimezone()->getName());
915     $this->assertEquals(1500000000, $datetimeplus->getTimestamp());
916     $this->assertEquals('America/New_York', $datetimeplus->getTimezone()->getName());
917   }
918
919 }