diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index b6205adc..00000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,6 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Unsafe call to private method Cake\\\\Chronos\\\\ChronosDate\\:\\:isRelativeOnly\\(\\) through static\\:\\:\\.$#" - count: 1 - path: src/ChronosDate.php diff --git a/phpstan.neon b/phpstan.neon index f3127314..fcfb25f3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,3 @@ -includes: - - phpstan-baseline.neon - parameters: level: 6 checkMissingIterableValueType: false diff --git a/psalm-baseline.xml b/psalm-baseline.xml index aa52d4ad..3d462e87 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -70,14 +70,16 @@ - + array_filter + date_default_timezone_get iterator_to_array - + $period diffForHumans diffFormatter + format getTestNow getWeekEndsAt getWeekEndsAt @@ -85,7 +87,6 @@ getWeekStartsAt getWeekendDays hasTestNow - isRelativeOnly modify modify modify @@ -114,7 +115,17 @@ now now now - stripTime + now + now + now + now + now + now + now + now + now + tomorrow + yesterday static::$days diff --git a/src/Chronos.php b/src/Chronos.php index 46b1a1ad..5cb60b08 100644 --- a/src/Chronos.php +++ b/src/Chronos.php @@ -240,7 +240,7 @@ protected function createNative( Chronos|ChronosDate|DateTimeInterface|string|int|null $time, DateTimeZone|string|null $timezone = null ): DateTimeImmutable { - if (is_int($time)) { + if (is_int($time) || (is_string($time) && ctype_digit($time))) { return new DateTimeImmutable("@{$time}"); } diff --git a/src/ChronosDate.php b/src/ChronosDate.php index e933ffee..d79e2650 100644 --- a/src/ChronosDate.php +++ b/src/ChronosDate.php @@ -18,6 +18,7 @@ use DatePeriod; use DateTimeImmutable; use DateTimeInterface; +use DateTimeZone; use InvalidArgumentException; /** @@ -90,35 +91,92 @@ class ChronosDate * subtraction/addition to have deterministic results. * * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string $time Fixed or relative time + * @param \DateTimeZone|string|null $timezone The time zone used for 'now' */ - public function __construct(Chronos|ChronosDate|DateTimeInterface|string $time) - { - $this->native = $this->createNative($time); + public function __construct( + Chronos|ChronosDate|DateTimeInterface|string $time = 'now', + DateTimeZone|string|null $timezone = null + ) { + $this->native = $this->createNative($time, $timezone); } /** * Initializes the PHP DateTimeImmutable object. * - * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int|null $time Fixed or relative time + * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string $time Fixed or relative time + * @param \DateTimeZone|string|null $timezone The time zone used for 'now' * @return \DateTimeImmutable */ - protected function createNative(Chronos|ChronosDate|DateTimeInterface|string|int|null $time): DateTimeImmutable - { + protected function createNative( + Chronos|ChronosDate|DateTimeInterface|string $time, + DateTimeZone|string|null $timezone + ): DateTimeImmutable { + if (!is_string($time)) { + return new DateTimeImmutable($time->format('Y-m-d 00:00:00')); + } + + $timezone ??= date_default_timezone_get(); + $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); + $testNow = Chronos::getTestNow(); - if ($testNow === null || !static::isRelativeOnly($time)) { - $time = $this->stripTime($time); + if ($testNow === null) { + $time = new DateTimeImmutable($time, $timezone); - return new DateTimeImmutable($time); + return new DateTimeImmutable($time->format('Y-m-d 00:00:00')); } - $testNow = clone $testNow; - if (!empty($time)) { + $testNow = $testNow->setTimezone($timezone); + if ($time !== 'now') { $testNow = $testNow->modify($time); } return new DateTimeImmutable($testNow->format('Y-m-d 00:00:00')); } + /** + * Get today's date. + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return static + */ + public static function now(DateTimeZone|string|null $timezone = null): static + { + return new static('now', $timezone); + } + + /** + * Get today's date. + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return static + */ + public static function today(DateTimeZone|string|null $timezone = null): static + { + return static::now($timezone); + } + + /** + * Get tomorrow's date. + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return static + */ + public static function tomorrow(DateTimeZone|string|null $timezone = null): static + { + return new static('tomorrow', $timezone); + } + + /** + * Get yesterday's date. + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return static + */ + public static function yesterday(DateTimeZone|string|null $timezone = null): static + { + return new static('yesterday', $timezone); + } + /** * Create an instance from a string. This is an alias for the * constructor that allows better fluent syntax as it allows you to do @@ -965,6 +1023,127 @@ public function isWeekend(): bool return in_array($this->dayOfWeek, Chronos::getWeekendDays(), true); } + /** + * Determines if the instance is yesterday + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isYesterday(DateTimeZone|string|null $timezone = null): bool + { + return $this->equals(static::yesterday($timezone)); + } + + /** + * Determines if the instance is today + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isToday(DateTimeZone|string|null $timezone = null): bool + { + return $this->equals(static::now($timezone)); + } + + /** + * Determines if the instance is tomorrow + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isTomorrow(DateTimeZone|string|null $timezone = null): bool + { + return $this->equals(static::tomorrow($timezone)); + } + + /** + * Determines if the instance is within the next week + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isNextWeek(DateTimeZone|string|null $timezone = null): bool + { + return $this->format('W o') === static::now($timezone)->addWeeks(1)->format('W o'); + } + + /** + * Determines if the instance is within the last week + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isLastWeek(DateTimeZone|string|null $timezone = null): bool + { + return $this->format('W o') === static::now($timezone)->subWeeks(1)->format('W o'); + } + + /** + * Determines if the instance is within the next month + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isNextMonth(DateTimeZone|string|null $timezone = null): bool + { + return $this->format('m Y') === static::now($timezone)->addMonths(1)->format('m Y'); + } + + /** + * Determines if the instance is within the last month + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isLastMonth(DateTimeZone|string|null $timezone = null): bool + { + return $this->format('m Y') === static::now($timezone)->subMonths(1)->format('m Y'); + } + + /** + * Determines if the instance is within the next year + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isNextYear(DateTimeZone|string|null $timezone = null): bool + { + return $this->year === static::now($timezone)->addYears(1)->year; + } + + /** + * Determines if the instance is within the last year + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isLastYear(DateTimeZone|string|null $timezone = null): bool + { + return $this->year === static::now($timezone)->subYears(1)->year; + } + + /** + * Determines if the instance is in the future, ie. greater (after) than now + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isFuture(DateTimeZone|string|null $timezone = null): bool + { + return $this->greaterThan(static::now($timezone)); + } + + /** + * Determines if the instance is in the past, ie. less (before) than now + * + * @param \DateTimeZone|string|null $timezone Time zone to use for now. + * @return bool + */ + public function isPast(DateTimeZone|string|null $timezone = null): bool + { + return $this->lessThan(static::now($timezone)); + } + /** * Determines if the instance is a leap year * diff --git a/src/Traits/FrozenTimeTrait.php b/src/Traits/FrozenTimeTrait.php index 4ea956b0..5a705ed6 100644 --- a/src/Traits/FrozenTimeTrait.php +++ b/src/Traits/FrozenTimeTrait.php @@ -13,11 +13,7 @@ */ namespace Cake\Chronos\Traits; -use Cake\Chronos\Chronos; -use Cake\Chronos\ChronosDate; use DateInterval; -use DateTimeImmutable; -use DateTimeInterface; use InvalidArgumentException; /** @@ -29,23 +25,6 @@ trait FrozenTimeTrait { use RelativeKeywordTrait; - /** - * Removes the time components from an input string. - * - * Used to ensure constructed objects always lack time. - * - * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string $time The input time - * @return string The date component of $time. - */ - protected function stripTime(Chronos|ChronosDate|DateTimeInterface|string $time): string - { - if (is_string($time)) { - $time = new DateTimeImmutable($time); - } - - return $time->format('Y-m-d 00:00:00'); - } - /** * Remove time components from strtotime relative strings. * diff --git a/tests/TestCase/Date/ConstructTest.php b/tests/TestCase/Date/ConstructTest.php index f5828c07..7be87b3e 100644 --- a/tests/TestCase/Date/ConstructTest.php +++ b/tests/TestCase/Date/ConstructTest.php @@ -18,6 +18,7 @@ use Cake\Chronos\Test\TestCase\TestCase; use DateTime; use DateTimeImmutable; +use DateTimeZone; /** * Test constructors for Date objects. @@ -97,6 +98,79 @@ public function testConstructWithRelative() $this->assertSame('2001-01-08', $c->format('Y-m-d')); } + public function testConstructWithLocalTimezone(): void + { + $londonTimezone = new DateTimeZone('Europe/London'); + + // now adjusted to London time + // This test could have different results depending on when now is + $c = new ChronosDate('now', $londonTimezone); + $london = new DateTimeImmutable('now', $londonTimezone); + $this->assertSame($london->format('Y-m-d'), $c->format('Y-m-d')); + + // now adjusted to London time + $c = ChronosDate::today($londonTimezone); + $this->assertSame(Chronos::today($londonTimezone)->format('Y-m-d'), $c->format('Y-m-d')); + + // London timezone is used instead of local timezone + $c = new ChronosDate('2001-01-02 01:00:00', $londonTimezone); + $this->assertSame('2001-01-02 00:00:00', $c->format('Y-m-d H:i:s')); + + // London timezone is ignored when timezone is provided in time string + $c = new ChronosDate('2001-01-01 23:00:00-400', $londonTimezone); + $this->assertSame('2001-01-01 00:00:00', $c->format('Y-m-d H:i:s')); + + // London timezone is ignored when DateTimeInterface instance is provided + $c = new ChronosDate(new DateTimeImmutable('2001-01-01 23:00:00-400'), $londonTimezone); + $this->assertSame('2001-01-01 00:00:00', $c->format('Y-m-d H:i:s')); + } + + public function testConstructWithLocalTimezoneTestNow(): void + { + Chronos::setTestNow(new Chronos('2010-01-01 23:00:00')); + + $londonTimezone = new DateTimeZone('Europe/London'); + + // TestNow is adjusted to London time + $c = new ChronosDate('now', $londonTimezone); + $this->assertSame('2010-01-02 00:00:00', $c->format('Y-m-d H:i:s')); + + // TestNow is adjusted to London time + $c = new ChronosDate('+2 days', $londonTimezone); + $this->assertSame('2010-01-04 00:00:00', $c->format('Y-m-d H:i:s')); + + // TestNow is adjusted to London time + $c = ChronosDate::today($londonTimezone); + $this->assertSame('2010-01-02 00:00:00', $c->format('Y-m-d H:i:s')); + $this->assertSame(Chronos::today($londonTimezone)->format('Y-m-d'), $c->format('Y-m-d')); + + // TestNow is adjusted to London time + $c = ChronosDate::tomorrow($londonTimezone); + $this->assertSame('2010-01-03 00:00:00', $c->format('Y-m-d H:i:s')); + $this->assertSame(Chronos::tomorrow($londonTimezone)->format('Y-m-d'), $c->format('Y-m-d')); + + // TestNow is ignored when specific date is provided + $c = new ChronosDate('2001-01-05 01:00:00', $londonTimezone); + $this->assertSame('2001-01-05 00:00:00', $c->format('Y-m-d H:i:s')); + } + + /** + * This tests with a large difference between local timezone and + * timezone provided as parameter. This is to help guarantee a date + * change would occur so the tests are more consistent. + */ + public function testConstructWithLargeTimezoneChange(): void + { + date_default_timezone_set('Pacific/Kiritimati'); + + $samoaTimezone = new DateTimeZone('Pacific/Samoa'); + + // Pacific/Samoa -11:00 is used intead of local timezone +14:00 + $c = ChronosDate::today($samoaTimezone); + $samoa = new DateTimeImmutable('now', $samoaTimezone); + $this->assertSame($samoa->format('Y-m-d'), $c->format('Y-m-d')); + } + public function testCreateFromExistingInstance() { $existingClass = new ChronosDate(new Chronos()); diff --git a/tests/TestCase/Date/IsTest.php b/tests/TestCase/Date/IsTest.php index 1f5acd3d..66a38873 100644 --- a/tests/TestCase/Date/IsTest.php +++ b/tests/TestCase/Date/IsTest.php @@ -40,6 +40,162 @@ public function testIsWeekendFalse() $this->assertFalse(ChronosDate::create(2012, 1, 2)->isWeekend()); } + public function testIsYesterdayTrue() + { + $this->assertTrue(ChronosDate::now()->subDays(1)->isYesterday()); + } + + public function testIsYesterdayFalseWithToday() + { + $this->assertFalse(ChronosDate::now()->isYesterday()); + } + + public function testIsYesterdayFalseWith2Days() + { + $this->assertFalse(ChronosDate::now()->subDays(2)->isYesterday()); + } + + public function testIsTodayTrue() + { + $this->assertTrue(ChronosDate::now()->isToday()); + } + + public function testIsTodayFalseWithYesterday() + { + $this->assertFalse(ChronosDate::now()->subDays(1)->isToday()); + } + + public function testIsTodayFalseWithTomorrow() + { + $this->assertFalse(ChronosDate::now()->addDays(1)->isToday()); + } + + public function isTodayFalseWithTimezone() + { + date_default_timezone_set('Pacific/Kiritimati'); + $samoaTimezone = new DateTimeZone('Pacific/Samoa'); + + // Pacific/Samoa -11:00 is used intead of local timezone +14:00 + $this->assertFalse(ChronosDate::now()->isToday($samoaTimezone)); + $this->assertTrue(ChronosDate::now()->isToday('Pacific/Kiritimati')); + } + + public function testIsTomorrowTrue() + { + $this->assertTrue(ChronosDate::now()->addDays(1)->isTomorrow()); + } + + public function testIsTomorrowFalseWithToday() + { + $this->assertFalse(ChronosDate::now()->isTomorrow()); + } + + public function testIsTomorrowFalseWith2Days() + { + $this->assertFalse(Chronos::now()->addDays(2)->isTomorrow()); + } + + public function testIsNextWeekTrue() + { + $this->assertTrue(ChronosDate::now()->addWeeks(1)->isNextWeek()); + } + + public function testIsLastWeekTrue() + { + $this->assertTrue(ChronosDate::now()->subWeeks(1)->isLastWeek()); + } + + public function testIsNextWeekFalse() + { + $this->assertFalse(ChronosDate::now()->addWeeks(2)->isNextWeek()); + + Chronos::setTestNow('2017-W01'); + $time = new ChronosDate('2018-W02'); + $this->assertFalse($time->isNextWeek()); + } + + public function testIsLastWeekFalse() + { + $this->assertFalse(ChronosDate::now()->subWeeks(2)->isLastWeek()); + + Chronos::setTestNow('2018-W02'); + $time = new ChronosDate('2017-W01'); + $this->assertFalse($time->isLastWeek()); + } + + public function testIsNextMonthTrue() + { + $this->assertTrue(ChronosDate::now()->addMonths(1)->isNextMonth()); + } + + public function testIsLastMonthTrue() + { + $this->assertTrue(ChronosDate::now()->subMonths(1)->isLastMonth()); + } + + public function testIsNextMonthFalse() + { + $this->assertFalse(ChronosDate::now()->addMonths(2)->isNextMonth()); + + Chronos::setTestNow('2017-12-31'); + $time = new ChronosDate('2017-01-01'); + $this->assertFalse($time->isNextMonth()); + } + + public function testIsLastMonthFalse() + { + $this->assertFalse(ChronosDate::now()->subMonths(2)->isLastMonth()); + + Chronos::setTestNow('2017-01-01'); + $time = new ChronosDate('2017-12-31'); + $this->assertFalse($time->isLastMonth()); + } + + public function testIsNextYearTrue() + { + $this->assertTrue(ChronosDate::now()->addYears(1)->isNextYear()); + } + + public function testIsLastYearTrue() + { + $this->assertTrue(ChronosDate::now()->subYears(1)->isLastYear()); + } + + public function testIsNextYearFalse() + { + $this->assertFalse(ChronosDate::now()->addYears(2)->isNextYear()); + } + + public function testIsLastYearFalse() + { + $this->assertFalse(ChronosDate::now()->subYears(2)->isLastYear()); + } + + public function testIsFutureTrue() + { + $this->assertTrue(ChronosDate::now()->addDays(1)->isFuture()); + } + + public function testIsFutureFalse() + { + $this->assertFalse(ChronosDate::now()->isFuture()); + } + + public function testIsFutureFalseInThePast() + { + $this->assertFalse(ChronosDate::now()->subDays(1)->isFuture()); + } + + public function testIsPastTrue() + { + $this->assertTrue(ChronosDate::now()->subDays(1)->isPast()); + } + + public function testIsPastFalse() + { + $this->assertFalse(ChronosDate::now()->addDays(1)->isPast()); + } + public function testIsLeapYearTrue() { $this->assertTrue(ChronosDate::create(2016, 1, 1)->isLeapYear()); diff --git a/tests/TestCase/DateTime/ConstructTest.php b/tests/TestCase/DateTime/ConstructTest.php index 3e08ee7b..4f1836b0 100644 --- a/tests/TestCase/DateTime/ConstructTest.php +++ b/tests/TestCase/DateTime/ConstructTest.php @@ -27,10 +27,16 @@ class ConstructTest extends TestCase public function testCreateFromTimestamp() { $ts = 1454284800; + $time = new Chronos($ts); + $this->assertSame('+00:00', $time->tzName); + $this->assertSame('2016-02-01 00:00:00', $time->format('Y-m-d H:i:s')); + $this->assertSame($ts, $time->getTimestamp()); + $ts = '1454284800'; $time = new Chronos($ts); $this->assertSame('+00:00', $time->tzName); $this->assertSame('2016-02-01 00:00:00', $time->format('Y-m-d H:i:s')); + $this->assertSame((int)$ts, $time->getTimestamp()); } public function testCreatesAnInstanceDefaultToNow()