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()