DateTimePlusTest.php 33.8 KB
Newer Older
1 2 3 4 5
<?php

namespace Drupal\Tests\Component\Datetime;

use Drupal\Component\Datetime\DateTimePlus;
6
use PHPUnit\Framework\TestCase;
7 8

/**
9 10
 * @coversDefaultClass \Drupal\Component\Datetime\DateTimePlus
 * @group Datetime
11
 */
12
class DateTimePlusTest extends TestCase {
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

  /**
   * Test creating dates from string and array input.
   *
   * @param mixed $input
   *   Input argument for DateTimePlus.
   * @param string $timezone
   *   Timezone argument for DateTimePlus.
   * @param string $expected
   *   Expected output from DateTimePlus::format().
   *
   * @dataProvider providerTestDates
   */
  public function testDates($input, $timezone, $expected) {
    $date = new DateTimePlus($input, $timezone);
    $value = $date->format('c');

    if (is_array($input)) {
      $input = var_export($input, TRUE);
    }
    $this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $timezone, $expected, $value));
  }

  /**
37
   * Test creating dates from string and array input.
38 39
   *
   * @param mixed $input
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
   *   Input argument for DateTimePlus.
   * @param string $timezone
   *   Timezone argument for DateTimePlus.
   * @param string $expected
   *   Expected output from DateTimePlus::format().
   *
   * @dataProvider providerTestDateArrays
   */
  public function testDateArrays($input, $timezone, $expected) {
    $date = DateTimePlus::createFromArray($input, $timezone);
    $value = $date->format('c');

    if (is_array($input)) {
      $input = var_export($input, TRUE);
    }
    $this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $timezone, $expected, $value));
  }

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
  /**
   * Test date diffs.
   *
   * @param mixed $input1
   *   A DateTimePlus object.
   * @param mixed $input2
   *   Date argument for DateTimePlus::diff method.
   * @param bool $absolute
   *   Absolute flag for DateTimePlus::diff method.
   * @param \DateInterval $expected
   *   The expected result of the DateTimePlus::diff operation.
   *
   * @dataProvider providerTestDateDiff
   */
  public function testDateDiff($input1, $input2, $absolute, \DateInterval $expected) {
    $interval = $input1->diff($input2, $absolute);
    $this->assertEquals($interval, $expected);
  }

  /**
   * Test date diff exception caused by invalid input.
   *
   * @param mixed $input1
   *   A DateTimePlus object.
   * @param mixed $input2
   *   Date argument for DateTimePlus::diff method.
   * @param bool $absolute
   *   Absolute flag for DateTimePlus::diff method.
   *
   * @dataProvider providerTestInvalidDateDiff
   */
  public function testInvalidDateDiff($input1, $input2, $absolute) {
90 91
    $this->expectException(\BadMethodCallException::class);
    $this->expectExceptionMessage('Method Drupal\Component\Datetime\DateTimePlus::diff expects parameter 1 to be a \DateTime or \Drupal\Component\Datetime\DateTimePlus object');
92 93 94
    $interval = $input1->diff($input2, $absolute);
  }

95 96 97 98 99 100 101
  /**
   * Test creating dates from invalid array input.
   *
   * @param mixed $input
   *   Input argument for DateTimePlus.
   * @param string $timezone
   *   Timezone argument for DateTimePlus.
102 103
   * @param string $class
   *   The Exception subclass to expect to be thrown.
104 105 106
   *
   * @dataProvider providerTestInvalidDateArrays
   */
107
  public function testInvalidDateArrays($input, $timezone, $class) {
108
    $this->expectException($class);
109 110 111 112 113 114
    $this->assertInstanceOf(
      '\Drupal\Component\DateTimePlus',
      DateTimePlus::createFromArray($input, $timezone)
    );
  }

115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
  /**
   * Tests DateTimePlus::checkArray().
   *
   * @param mixed $array
   *   Input argument for DateTimePlus::checkArray().
   * @param bool $expected
   *   The expected result of DateTimePlus::checkArray().
   *
   * @dataProvider providerTestCheckArray
   */
  public function testCheckArray(array $array, $expected) {
    $this->assertSame(
      $expected,
      DateTimePlus::checkArray($array)
    );
  }

132 133 134 135 136
  /**
   * Test creating dates from timestamps, and manipulating timezones.
   *
   * @param int $input
   *   Input argument for DateTimePlus::createFromTimestamp().
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
   * @param array $initial
   *   An array containing:
   *   - 'timezone_initial' - Timezone argument for DateTimePlus.
   *   - 'format_initial' - Format argument for DateTimePlus.
   *   - 'expected_initial_date' - Expected output from DateTimePlus::format().
   *   - 'expected_initial_timezone' - Expected output from
   *      DateTimePlus::getTimeZone()::getName().
   *   - 'expected_initial_offset' - Expected output from DateTimePlus::getOffset().
   * @param array $transform
   *   An array containing:
   *   - 'timezone_transform' - Argument to transform date to another timezone via
   *     DateTimePlus::setTimezone().
   *   - 'format_transform' - Format argument to use when transforming date to
   *     another timezone.
   *   - 'expected_transform_date' - Expected output from DateTimePlus::format(),
   *     after timezone transform.
   *   - 'expected_transform_timezone' - Expected output from
   *     DateTimePlus::getTimeZone()::getName(), after timezone transform.
   *   - 'expected_transform_offset' - Expected output from
   *      DateTimePlus::getOffset(), after timezone transform.
   *
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
   * @dataProvider providerTestTimestamp
   */
  public function testTimestamp($input, array $initial, array $transform) {
    // Initialize a new date object.
    $date = DateTimePlus::createFromTimestamp($input, $initial['timezone']);
    $this->assertDateTimestamp($date, $input, $initial, $transform);
  }

  /**
   * Test creating dates from datetime strings.
   *
   * @param string $input
   *   Input argument for DateTimePlus().
   * @param array $initial
   *   @see testTimestamp()
   * @param array $transform
   *   @see testTimestamp()
   *
176 177 178 179 180
   * @dataProvider providerTestDateTimestamp
   */
  public function testDateTimestamp($input, array $initial, array $transform) {
    // Initialize a new date object.
    $date = new DateTimePlus($input, $initial['timezone']);
181 182
    $this->assertDateTimestamp($date, $input, $initial, $transform);
  }
183

184 185 186 187
  /**
   * Assertion helper for testTimestamp and testDateTimestamp since they need
   * different dataProviders.
   *
188
   * @param \Drupal\Component\Datetime\DateTimePlus $date
189 190 191 192 193 194 195 196 197
   *   DateTimePlus to test.
   * @input mixed $input
   *   The original input passed to the test method.
   * @param array $initial
   *   @see testTimestamp()
   * @param array $transform
   *   @see testTimestamp()
   */
  public function assertDateTimestamp($date, $input, $initial, $transform) {
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
    // Check format.
    $value = $date->format($initial['format']);
    $this->assertEquals($initial['expected_date'], $value, sprintf("Test new DateTimePlus(%s, %s): should be %s, found %s.", $input, $initial['timezone'], $initial['expected_date'], $value));

    // Check timezone name.
    $value = $date->getTimeZone()->getName();
    $this->assertEquals($initial['expected_timezone'], $value, sprintf("The current timezone is %s: should be %s.", $value, $initial['expected_timezone']));

    // Check offset.
    $value = $date->getOffset();
    $this->assertEquals($initial['expected_offset'], $value, sprintf("The current offset is %s: should be %s.", $value, $initial['expected_offset']));

    // Transform the date to another timezone.
    $date->setTimezone(new \DateTimeZone($transform['timezone']));

    // Check transformed format.
    $value = $date->format($transform['format']);
    $this->assertEquals($transform['expected_date'], $value, sprintf("Test \$date->setTimezone(new \\DateTimeZone(%s)): should be %s, found %s.", $transform['timezone'], $transform['expected_date'], $value));

    // Check transformed timezone.
    $value = $date->getTimeZone()->getName();
    $this->assertEquals($transform['expected_timezone'], $value, sprintf("The current timezone should be %s, found %s.", $transform['expected_timezone'], $value));

    // Check transformed offset.
    $value = $date->getOffset();
    $this->assertEquals($transform['expected_offset'], $value, sprintf("The current offset should be %s, found %s.", $transform['expected_offset'], $value));
  }

  /**
   * Test creating dates from format strings.
   *
   * @param string $input
   *   Input argument for DateTimePlus.
   * @param string $timezone
   *   Timezone argument for DateTimePlus.
   * @param string $format_date
   *   Format argument for DateTimePlus::format().
   * @param string $expected
   *   Expected output from DateTimePlus::format().
   *
   * @dataProvider providerTestDateFormat
   */
  public function testDateFormat($input, $timezone, $format, $format_date, $expected) {
241
    $date = DateTimePlus::createFromFormat($format, $input, $timezone);
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
    $value = $date->format($format_date);
    $this->assertEquals($expected, $value, sprintf("Test new DateTimePlus(%s, %s, %s): should be %s, found %s.", $input, $timezone, $format, $expected, $value));
  }

  /**
   * Test invalid date handling.
   *
   * @param mixed $input
   *   Input argument for DateTimePlus.
   * @param string $timezone
   *   Timezone argument for DateTimePlus.
   * @param string $format
   *   Format argument for DateTimePlus.
   * @param string $message
   *   Message to print if no errors are thrown by the invalid dates.
257 258
   * @param string $class
   *   The Exception subclass to expect to be thrown.
259 260 261
   *
   * @dataProvider providerTestInvalidDates
   */
262
  public function testInvalidDates($input, $timezone, $format, $message, $class) {
263
    $this->expectException($class);
264
    DateTimePlus::createFromFormat($format, $input, $timezone);
265 266 267
  }

  /**
268
   * Tests that DrupalDateTime can detect the right timezone to use.
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
   * When specified or not.
   *
   * @param mixed $input
   *   Input argument for DateTimePlus.
   * @param mixed $timezone
   *   Timezone argument for DateTimePlus.
   * @param string $expected_timezone
   *   Expected timezone returned from DateTimePlus::getTimezone::getName().
   * @param string $message
   *   Message to print on test failure.
   *
   * @dataProvider providerTestDateTimezone
   */
  public function testDateTimezone($input, $timezone, $expected_timezone, $message) {
    $date = new DateTimePlus($input, $timezone);
    $timezone = $date->getTimezone()->getName();
    $this->assertEquals($timezone, $expected_timezone, $message);
  }

  /**
289 290 291 292 293
   * Test that DrupalDateTime can detect the right timezone to use when
   * constructed from a datetime object.
   */
  public function testDateTimezoneWithDateTimeObject() {
    // Create a date object with another date object.
294
    $input = new \DateTime('now', new \DateTimeZone('Pacific/Midway'));
295 296 297 298 299 300 301 302 303 304 305
    $timezone = NULL;
    $expected_timezone = 'Pacific/Midway';
    $message = 'DateTimePlus uses the specified timezone if provided.';

    $date = DateTimePlus::createFromDateTime($input, $timezone);
    $timezone = $date->getTimezone()->getName();
    $this->assertEquals($timezone, $expected_timezone, $message);
  }

  /**
   * Provides data for date tests.
306 307 308 309 310
   *
   * @return array
   *   An array of arrays, each containing the input parameters for
   *   DateTimePlusTest::testDates().
   *
311
   * @see DateTimePlusTest::testDates()
312 313
   */
  public function providerTestDates() {
314
    $dates = [
315 316
      // String input.
      // Create date object from datetime string.
317
      ['2009-03-07 10:30', 'America/Chicago', '2009-03-07T10:30:00-06:00'],
318
      // Same during daylight savings time.
319
      ['2009-06-07 10:30', 'America/Chicago', '2009-06-07T10:30:00-05:00'],
320
      // Create date object from date string.
321
      ['2009-03-07', 'America/Chicago', '2009-03-07T00:00:00-06:00'],
322
      // Same during daylight savings time.
323
      ['2009-06-07', 'America/Chicago', '2009-06-07T00:00:00-05:00'],
324
      // Create date object from date string.
325
      ['2009-03-07 10:30', 'Australia/Canberra', '2009-03-07T10:30:00+11:00'],
326
      // Same during daylight savings time.
327 328
      ['2009-06-07 10:30', 'Australia/Canberra', '2009-06-07T10:30:00+10:00'],
    ];
329 330 331 332 333

    // On 32-bit systems, timestamps are limited to 1901-2038.
    if (PHP_INT_SIZE > 4) {
      // Create a date object in the distant past.
      // @see https://www.drupal.org/node/2795489#comment-12127088
334 335 336
      // Note that this date is after the United States standardized its
      // timezones.
      $dates[] = ['1883-11-19 10:30', 'America/Chicago', '1883-11-19T10:30:00-06:00'];
337 338 339 340 341
      // Create a date object in the far future.
      $dates[] = ['2345-01-02 02:04', 'UTC', '2345-01-02T02:04:00+00:00'];
    }

    return $dates;
342
  }
343

344 345 346 347 348 349 350
  /**
   * Provides data for date tests.
   *
   * @return array
   *   An array of arrays, each containing the input parameters for
   *   DateTimePlusTest::testDates().
   *
351
   * @see DateTimePlusTest::testDates()
352 353
   */
  public function providerTestDateArrays() {
354
    $dates = [
355 356
      // Array input.
      // Create date object from date array, date only.
357
      [['year' => 2010, 'month' => 2, 'day' => 28], 'America/Chicago', '2010-02-28T00:00:00-06:00'],
358
      // Create date object from date array with hour.
359
      [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], 'America/Chicago', '2010-02-28T10:00:00-06:00'],
360
      // Create date object from date array, date only.
361
      [['year' => 2010, 'month' => 2, 'day' => 28], 'Europe/Berlin', '2010-02-28T00:00:00+01:00'],
362
      // Create date object from date array with hour.
363 364
      [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], 'Europe/Berlin', '2010-02-28T10:00:00+01:00'],
    ];
365 366 367 368 369

    // On 32-bit systems, timestamps are limited to 1901-2038.
    if (PHP_INT_SIZE > 4) {
      // Create a date object in the distant past.
      // @see https://www.drupal.org/node/2795489#comment-12127088
370 371 372
      // Note that this date is after the United States standardized its
      // timezones.
      $dates[] = [['year' => 1883, 'month' => 11, 'day' => 19], 'America/Chicago', '1883-11-19T00:00:00-06:00'];
373 374 375 376 377
      // Create a date object in the far future.
      $dates[] = [['year' => 2345, 'month' => 1, 'day' => 2], 'UTC', '2345-01-02T00:00:00+00:00'];
    }

    return $dates;
378 379 380
  }

  /**
381
   * Provides data for testDateFormats.
382 383 384 385 386 387 388 389 390 391 392 393
   *
   * @return array
   *   An array of arrays, each containing:
   *   - 'input' - Input to DateTimePlus.
   *   - 'timezone' - Timezone for DateTimePlus.
   *   - 'format' - Date format for DateTimePlus.
   *   - 'format_date' - Date format for use in $date->format() method.
   *   - 'expected' - The expected return from DateTimePlus.
   *
   * @see testDateFormats()
   */
  public function providerTestDateFormat() {
394
    return [
395
      // Create a year-only date.
396
      ['2009', NULL, 'Y', 'Y', '2009'],
397
      // Create a month and year-only date.
398
      ['2009-10', NULL, 'Y-m', 'Y-m', '2009-10'],
399
      // Create a time-only date.
400
      ['T10:30:00', NULL, '\TH:i:s', 'H:i:s', '10:30:00'],
401
      // Create a time-only date.
402 403
      ['10:30:00', NULL, 'H:i:s', 'H:i:s', '10:30:00'],
    ];
404 405 406
  }

  /**
407
   * Provides data for testInvalidDates.
408 409 410 411 412 413 414 415 416 417 418
   *
   * @return array
   *   An array of arrays, each containing:
   *   - 'input' - Input for DateTimePlus.
   *   - 'timezone' - Timezone for DateTimePlus.
   *   - 'format' - Format for DateTimePlus.
   *   - 'message' - Message to display on failure.
   *
   * @see testInvalidDates
   */
  public function providerTestInvalidDates() {
419
    return [
420 421
      // Test for invalid month names when we are using a short version
      // of the month.
422
      ['23 abc 2012', NULL, 'd M Y', "23 abc 2012 contains an invalid month name and did not produce errors.", \InvalidArgumentException::class],
423
      // Test for invalid hour.
424
      ['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],
425
      // Test for invalid day.
426
      ['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],
427
      // Test for invalid month.
428
      ['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],
429
      // Test for invalid year.
430
      ['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],
431

432
    ];
433 434 435
  }

  /**
436
   * Data provider for testInvalidDateArrays.
437 438 439 440 441 442 443 444 445
   *
   * @return array
   *   An array of arrays, each containing:
   *   - 'input' - Input for DateTimePlus.
   *   - 'timezone' - Timezone for DateTimePlus.
   *
   * @see testInvalidDateArrays
   */
  public function providerTestInvalidDateArrays() {
446
    return [
447
      // One year larger than the documented upper limit of checkdate().
448
      [['year' => 32768, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
449
      // One year smaller than the documented lower limit of checkdate().
450
      [['year' => 0, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
451
      // Test for invalid month from date array.
452
      [['year' => 2010, 'month' => 27, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
453
      // Test for invalid hour from date array.
454
      [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 80, 'minute' => 0, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
455
      // Test for invalid minute from date array.
456
      [['year' => 2010, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 88, 'second' => 0], 'America/Chicago', \InvalidArgumentException::class],
457
      // Regression test for https://www.drupal.org/node/2084455.
458 459
      [['hour' => 59, 'minute' => 1, 'second' => 1], 'America/Chicago', \InvalidArgumentException::class],
    ];
460 461
  }

462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
  /**
   * Data provider for testCheckArray.
   *
   * @return array
   *   An array of arrays, each containing:
   *   - 'array' - Input for DateTimePlus::checkArray().
   *   - 'expected' - Expected output for  DateTimePlus::checkArray().
   *
   * @see testCheckArray
   */
  public function providerTestCheckArray() {
    return [
      'Date array, date only' => [['year' => 2010, 'month' => 2, 'day' => 28], TRUE],
      'Date array with hour' => [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10], TRUE],
      'One year larger than the documented upper limit of checkdate()' => [['year' => 32768, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], FALSE],
      'One year smaller than the documented lower limit of checkdate()' => [['year' => 0, 'month' => 1, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], FALSE],
      'Invalid month from date array' => [['year' => 2010, 'month' => 27, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0], FALSE],
      'Invalid hour from date array' => [['year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 80, 'minute' => 0, 'second' => 0], FALSE],
      'Invalid minute from date array.' => [['year' => 2010, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 88, 'second' => 0], FALSE],
      'Missing day' => [['year' => 2059, 'month' => 1, 'second' => 1], FALSE],
      'Zero day' => [['year' => 2059, 'month' => 1, 'day' => 0], FALSE],
    ];
  }

486
  /**
487
   * Provides data for testDateTimezone.
488 489 490 491 492 493
   *
   * @return array
   *   An array of arrays, each containing:
   *   - 'date' - Date string or object for DateTimePlus.
   *   - 'timezone' - Timezone string for DateTimePlus.
   *   - 'expected' - Expected return from DateTimePlus::getTimezone()::getName().
494
   *   - 'message' - Message to display on test failure.
495 496 497 498 499 500 501 502 503 504
   *
   * @see testDateTimezone
   */
  public function providerTestDateTimezone() {
    // Use a common date for most of the tests.
    $date_string = '2007-01-31 21:00:00';

    // Detect the system timezone.
    $system_timezone = date_default_timezone_get();

505
    return [
506 507
      // Create a date object with an unspecified timezone, which should
      // end up using the system timezone.
508
      [$date_string, NULL, $system_timezone, 'DateTimePlus uses the system timezone when there is no site timezone.'],
509
      // Create a date object with a specified timezone name.
510
      [$date_string, 'America/Yellowknife', 'America/Yellowknife', 'DateTimePlus uses the specified timezone if provided.'],
511
      // Create a date object with a timezone object.
512
      [$date_string, new \DateTimeZone('Australia/Canberra'), 'Australia/Canberra', 'DateTimePlus uses the specified timezone if provided.'],
513
      // Create a date object with another date object.
514 515
      [new DateTimePlus('now', 'Pacific/Midway'), NULL, 'Pacific/Midway', 'DateTimePlus uses the specified timezone if provided.'],
    ];
516 517 518
  }

  /**
519
   * Provides data for testTimestamp.
520 521 522
   *
   * @return array
   *   An array of arrays, each containing the arguments required for
523
   *   self::testTimestamp().
524
   *
525
   * @see testTimestamp()
526
   */
527
  public function providerTestTimestamp() {
528
    return [
529 530
      // Create date object from a unix timestamp and display it in
      // local time.
531
      [
532
        'input' => 0,
533
        'initial' => [
534 535 536 537 538
          'timezone' => 'UTC',
          'format' => 'c',
          'expected_date' => '1970-01-01T00:00:00+00:00',
          'expected_timezone' => 'UTC',
          'expected_offset' => 0,
539 540
        ],
        'transform' => [
541 542 543 544 545
          'timezone' => 'America/Los_Angeles',
          'format' => 'c',
          'expected_date' => '1969-12-31T16:00:00-08:00',
          'expected_timezone' => 'America/Los_Angeles',
          'expected_offset' => '-28800',
546 547
        ],
      ],
548 549
      // Create a date using the timestamp of zero, then display its
      // value both in UTC and the local timezone.
550
      [
551
        'input' => 0,
552
        'initial' => [
553 554 555 556 557
          'timezone' => 'America/Los_Angeles',
          'format' => 'c',
          'expected_date' => '1969-12-31T16:00:00-08:00',
          'expected_timezone' => 'America/Los_Angeles',
          'expected_offset' => '-28800',
558 559
        ],
        'transform' => [
560 561 562 563 564
          'timezone' => 'UTC',
          'format' => 'c',
          'expected_date' => '1970-01-01T00:00:00+00:00',
          'expected_timezone' => 'UTC',
          'expected_offset' => 0,
565 566 567
        ],
      ],
    ];
568 569 570 571 572 573 574 575 576 577 578 579
  }

  /**
   * Provides data for testDateTimestamp.
   *
   * @return array
   *   An array of arrays, each containing the arguments required for
   *   self::testDateTimestamp().
   *
   * @see testDateTimestamp()
   */
  public function providerTestDateTimestamp() {
580
    return [
581 582
      // Create date object from datetime string in UTC, and convert
      // it to a local date.
583
      [
584
        'input' => '1970-01-01 00:00:00',
585
        'initial' => [
586 587 588 589 590
          'timezone' => 'UTC',
          'format' => 'c',
          'expected_date' => '1970-01-01T00:00:00+00:00',
          'expected_timezone' => 'UTC',
          'expected_offset' => 0,
591 592
        ],
        'transform' => [
593 594 595 596 597
          'timezone' => 'America/Los_Angeles',
          'format' => 'c',
          'expected_date' => '1969-12-31T16:00:00-08:00',
          'expected_timezone' => 'America/Los_Angeles',
          'expected_offset' => '-28800',
598 599
        ],
      ],
600
      // Convert the local time to UTC using string input.
601
      [
602
        'input' => '1969-12-31 16:00:00',
603
        'initial' => [
604 605 606 607 608
          'timezone' => 'America/Los_Angeles',
          'format' => 'c',
          'expected_date' => '1969-12-31T16:00:00-08:00',
          'expected_timezone' => 'America/Los_Angeles',
          'expected_offset' => '-28800',
609 610
        ],
        'transform' => [
611 612 613 614 615
          'timezone' => 'UTC',
          'format' => 'c',
          'expected_date' => '1970-01-01T00:00:00+00:00',
          'expected_timezone' => 'UTC',
          'expected_offset' => 0,
616 617
        ],
      ],
618
      // Convert the local time to UTC using string input.
619
      [
620
        'input' => '1969-12-31 16:00:00',
621
        'initial' => [
622 623 624 625 626
          'timezone' => 'Europe/Warsaw',
          'format' => 'c',
          'expected_date' => '1969-12-31T16:00:00+01:00',
          'expected_timezone' => 'Europe/Warsaw',
          'expected_offset' => '+3600',
627 628
        ],
        'transform' => [
629 630 631 632 633
          'timezone' => 'UTC',
          'format' => 'c',
          'expected_date' => '1969-12-31T15:00:00+00:00',
          'expected_timezone' => 'UTC',
          'expected_offset' => 0,
634 635 636
        ],
      ],
    ];
637 638
  }

639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
  /**
   * Provides data for date tests.
   *
   * @return array
   *   An array of arrays, each containing the input parameters for
   *   DateTimePlusTest::testDateDiff().
   *
   * @see DateTimePlusTest::testDateDiff()
   */
  public function providerTestDateDiff() {

    $empty_interval = new \DateInterval('PT0S');

    $positive_19_hours = new \DateInterval('PT19H');

    $positive_18_hours = new \DateInterval('PT18H');

    $positive_1_hour = new \DateInterval('PT1H');

    $negative_1_hour = new \DateInterval('PT1H');
    $negative_1_hour->invert = 1;

661
    return [
662 663
      // There should be a 19 hour time interval between
      // new years in Sydney and new years in LA in year 2000.
664
      [
665 666 667 668
        'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
        'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
        'absolute' => FALSE,
        'expected' => $positive_19_hours,
669
      ],
670 671
      // In 1970 Sydney did not observe daylight savings time
      // So there is only a 18 hour time interval.
672
      [
673 674 675 676
        'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
        'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
        'absolute' => FALSE,
        'expected' => $positive_18_hours,
677 678
      ],
      [
679 680 681 682
        'input1' => DateTimePlus::createFromFormat('U', 3600, new \DateTimeZone('America/Los_Angeles')),
        'input2' => DateTimePlus::createFromFormat('U', 0, new \DateTimeZone('UTC')),
        'absolute' => FALSE,
        'expected' => $negative_1_hour,
683 684
      ],
      [
685 686 687 688
        'input1' => DateTimePlus::createFromFormat('U', 3600),
        'input2' => DateTimePlus::createFromFormat('U', 0),
        'absolute' => FALSE,
        'expected' => $negative_1_hour,
689 690
      ],
      [
691 692 693 694
        'input1' => DateTimePlus::createFromFormat('U', 3600),
        'input2' => \DateTime::createFromFormat('U', 0),
        'absolute' => FALSE,
        'expected' => $negative_1_hour,
695 696
      ],
      [
697 698 699 700
        'input1' => DateTimePlus::createFromFormat('U', 3600),
        'input2' => DateTimePlus::createFromFormat('U', 0),
        'absolute' => TRUE,
        'expected' => $positive_1_hour,
701 702
      ],
      [
703 704 705 706
        'input1' => DateTimePlus::createFromFormat('U', 3600),
        'input2' => \DateTime::createFromFormat('U', 0),
        'absolute' => TRUE,
        'expected' => $positive_1_hour,
707 708
      ],
      [
709 710 711 712
        'input1' => DateTimePlus::createFromFormat('U', 0),
        'input2' => DateTimePlus::createFromFormat('U', 0),
        'absolute' => FALSE,
        'expected' => $empty_interval,
713 714
      ],
    ];
715 716 717 718 719 720 721 722 723 724 725 726
  }

  /**
   * Provides data for date tests.
   *
   * @return array
   *   An array of arrays, each containing the input parameters for
   *   DateTimePlusTest::testInvalidDateDiff().
   *
   * @see DateTimePlusTest::testInvalidDateDiff()
   */
  public function providerTestInvalidDateDiff() {
727 728
    return [
      [
729 730 731
        'input1' => DateTimePlus::createFromFormat('U', 3600),
        'input2' => '1970-01-01 00:00:00',
        'absolute' => FALSE,
732 733
      ],
      [
734 735 736
        'input1' => DateTimePlus::createFromFormat('U', 3600),
        'input2' => NULL,
        'absolute' => FALSE,
737 738
      ],
    ];
739 740
  }

741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
  /**
   * Tests invalid values passed to constructor.
   *
   * @param string $time
   *   A date/time string.
   * @param string[] $errors
   *   An array of error messages.
   *
   * @covers ::__construct
   *
   * @dataProvider providerTestInvalidConstructor
   */
  public function testInvalidConstructor($time, array $errors) {
    $date = new DateTimePlus($time);

    $this->assertEquals(TRUE, $date->hasErrors());
    $this->assertEquals($errors, $date->getErrors());
  }

  /**
   * Provider for testInvalidConstructor().
   *
   * @return array
   *   An array of invalid date/time strings, and corresponding error messages.
   */
  public function providerTestInvalidConstructor() {
    return [
      [
        'YYYY-MM-DD',
        [
          'The timezone could not be found in the database',
          'Unexpected character',
          'Double timezone specification',
        ],
      ],
      [
        '2017-MM-DD',
        [
          'Unexpected character',
          'The timezone could not be found in the database',
        ],
      ],
      [
        'YYYY-03-DD',
        [
          'The timezone could not be found in the database',
          'Unexpected character',
          'Double timezone specification',
        ],
      ],
      [
        'YYYY-MM-07',
        [
          'The timezone could not be found in the database',
          'Unexpected character',
          'Double timezone specification',
        ],
      ],
      [
        '2017-13-55',
        [
          'Unexpected character',
        ],
      ],
      [
        'YYYY-MM-DD hh:mm:ss',
        [
          'The timezone could not be found in the database',
          'Unexpected character',
          'Double timezone specification',
        ],
      ],
      [
        '2017-03-07 25:70:80',
        [
          'Unexpected character',
          'Double time specification',
        ],
      ],
      [
        'lorem ipsum dolor sit amet',
        [
          'The timezone could not be found in the database',
          'Double timezone specification',
        ],
      ],
    ];
  }

830 831 832 833 834 835
  /**
   * Tests the $settings['validate_format'] parameter in ::createFromFormat().
   */
  public function testValidateFormat() {
    // Check that an input that does not strictly follow the input format will
    // produce the desired date. In this case the year string '11' doesn't
836
    // precisely match the 'Y' formatter parameter, but PHP will parse it
837 838 839 840 841 842 843 844
    // regardless. However, when formatted with the same string, the year will
    // be output with four digits. With the ['validate_format' => FALSE]
    // $settings, this will not thrown an exception.
    $date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '11-03-31 17:44:00', 'UTC', ['validate_format' => FALSE]);
    $this->assertEquals('0011-03-31 17:44:00', $date->format('Y-m-d H:i:s'));

    // Parse the same date with ['validate_format' => TRUE] and make sure we
    // get the expected exception.
845
    $this->expectException(\UnexpectedValueException::class);
846 847 848
    $date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '11-03-31 17:44:00', 'UTC', ['validate_format' => TRUE]);
  }

849 850 851 852 853 854 855 856 857 858 859 860
  /**
   * Tests setting the default time for date-only objects.
   */
  public function testDefaultDateTime() {
    $utc = new \DateTimeZone('UTC');

    $date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '2017-05-23 22:58:00', $utc);
    $this->assertEquals('22:58:00', $date->format('H:i:s'));
    $date->setDefaultDateTime();
    $this->assertEquals('12:00:00', $date->format('H:i:s'));
  }

861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903
  /**
   * Tests that object methods are chainable.
   *
   * @covers ::__call
   */
  public function testChainable() {
    $date = new DateTimePlus('now', 'Australia/Sydney');

    $date->setTimestamp(12345678);
    $rendered = $date->render();
    $this->assertEquals('1970-05-24 07:21:18 Australia/Sydney', $rendered);

    $date->setTimestamp(23456789);
    $rendered = $date->setTimezone(new \DateTimeZone('America/New_York'))->render();
    $this->assertEquals('1970-09-29 07:46:29 America/New_York', $rendered);

    $date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-05-24 07:21:18', new \DateTimeZone('Australia/Sydney'))
      ->setTimezone(new \DateTimeZone('America/New_York'));
    $rendered = $date->render();
    $this->assertInstanceOf(DateTimePlus::class, $date);
    $this->assertEquals(12345678, $date->getTimestamp());
    $this->assertEquals('1970-05-23 17:21:18 America/New_York', $rendered);
  }

  /**
   * Tests that non-chainable methods work.
   *
   * @covers ::__call
   */
  public function testChainableNonChainable() {
    $datetime1 = new DateTimePlus('2009-10-11 12:00:00');
    $datetime2 = new DateTimePlus('2009-10-13 12:00:00');
    $interval = $datetime1->diff($datetime2);
    $this->assertInstanceOf(\DateInterval::class, $interval);
    $this->assertEquals('+2 days', $interval->format('%R%a days'));
  }

  /**
   * Tests that chained calls to non-existent functions throw an exception.
   *
   * @covers ::__call
   */
  public function testChainableNonCallable() {
904 905
    $this->expectException(\BadMethodCallException::class);
    $this->expectExceptionMessage('Call to undefined method Drupal\Component\Datetime\DateTimePlus::nonexistent()');
906 907 908 909
    $date = new DateTimePlus('now', 'Australia/Sydney');
    $date->setTimezone(new \DateTimeZone('America/New_York'))->nonexistent();
  }

910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934
  /**
   * @covers ::getPhpDateTime
   */
  public function testGetPhpDateTime() {
    $new_york = new \DateTimeZone('America/New_York');
    $berlin = new \DateTimeZone('Europe/Berlin');

    // Test retrieving a cloned copy of the wrapped \DateTime object, and that
    // altering it does not change the DateTimePlus object.
    $datetimeplus = DateTimePlus::createFromFormat('Y-m-d H:i:s', '2017-07-13 22:40:00', $new_york, ['langcode' => 'en']);
    $this->assertEquals(1500000000, $datetimeplus->getTimestamp());
    $this->assertEquals('America/New_York', $datetimeplus->getTimezone()->getName());

    $datetime = $datetimeplus->getPhpDateTime();
    $this->assertInstanceOf('DateTime', $datetime);
    $this->assertEquals(1500000000, $datetime->getTimestamp());
    $this->assertEquals('America/New_York', $datetime->getTimezone()->getName());

    $datetime->setTimestamp(1400000000)->setTimezone($berlin);
    $this->assertEquals(1400000000, $datetime->getTimestamp());
    $this->assertEquals('Europe/Berlin', $datetime->getTimezone()->getName());
    $this->assertEquals(1500000000, $datetimeplus->getTimestamp());
    $this->assertEquals('America/New_York', $datetimeplus->getTimezone()->getName());
  }

935
}