From a7011fc0c53b0eb021614946abb53253aa9a56a5 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sat, 14 Mar 2026 18:10:56 +0100 Subject: [PATCH] Auto-append pipe modifier in createFromFormat for deterministic behavior Unlike PHP's native DateTimeImmutable::createFromFormat(), this method now automatically appends the | modifier if no reset modifier (| or \!) is present. This ensures that unparsed components are reset to zero instead of being filled from the current time. This provides more predictable and deterministic behavior, especially useful for testing where flaky tests can occur due to missing components being filled with the current system time. Examples: - createFromFormat('Y-m-d', '2024-03-14') -> time is 00:00:00 (not current) - createFromFormat('Y-m-d H:i', '2024-03-14 12:30') -> seconds is 00 This is a behavior change from PHP's native method, but arguably the more intuitive default. --- src/Chronos.php | 12 ++++ .../DateTime/CreateFromFormatTest.php | 61 ++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/Chronos.php b/src/Chronos.php index eadcbb1..8957388 100644 --- a/src/Chronos.php +++ b/src/Chronos.php @@ -668,6 +668,12 @@ public static function createFromTime( /** * Create an instance from a specific format * + * Unlike PHP's native DateTimeImmutable::createFromFormat(), this method + * automatically appends the `|` modifier if no reset modifier (`|` or `!`) + * is present. This ensures that unparsed components are reset to zero + * instead of being filled from the current time, providing more predictable + * and deterministic behavior. + * * @param string $format The date() compatible format string. * @param string $time The formatted date string to interpret. * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name the new instance should use. @@ -679,6 +685,12 @@ public static function createFromFormat( string $time, DateTimeZone|string|null $timezone = null, ): static { + // Auto-append | modifier if no reset modifier is present + // This ensures unparsed components are zero instead of current time + if (!str_contains($format, '|') && !str_contains($format, '!')) { + $format .= '|'; + } + if ($timezone !== null) { $dateTime = parent::createFromFormat($format, $time, $timezone ? static::safeCreateDateTimeZone($timezone) : null); } else { diff --git a/tests/TestCase/DateTime/CreateFromFormatTest.php b/tests/TestCase/DateTime/CreateFromFormatTest.php index f124679..a98d240 100644 --- a/tests/TestCase/DateTime/CreateFromFormatTest.php +++ b/tests/TestCase/DateTime/CreateFromFormatTest.php @@ -29,6 +29,49 @@ public function testCreateFromFormatReturnsInstance() $this->assertTrue($d instanceof Chronos); } + public function testCreateFromFormatMissingTimeIsZero() + { + // Missing time components should be zero, not current time + $d = Chronos::createFromFormat('Y-m-d', '2024-03-14'); + $this->assertDateTime($d, 2024, 3, 14, 0, 0, 0); + $this->assertSame(0, $d->micro); + } + + public function testCreateFromFormatMissingSecondsIsZero() + { + // Missing seconds should be zero + $d = Chronos::createFromFormat('Y-m-d H:i', '2024-03-14 12:30'); + $this->assertDateTime($d, 2024, 3, 14, 12, 30, 0); + } + + public function testCreateFromFormatMissingDateIsEpoch() + { + // Missing date components should be Unix epoch (1970-01-01) + $d = Chronos::createFromFormat('H:i:s', '12:30:45'); + $this->assertDateTime($d, 1970, 1, 1, 12, 30, 45); + } + + public function testCreateFromFormatMissingMicrosecondsIsZero() + { + // Missing microseconds should be zero + $d = Chronos::createFromFormat('Y-m-d H:i:s', '2024-03-14 12:30:45'); + $this->assertSame(0, $d->micro); + } + + public function testCreateFromFormatWithExplicitPipeModifier() + { + // Explicit | should still work + $d = Chronos::createFromFormat('Y-m-d|', '2024-03-14'); + $this->assertDateTime($d, 2024, 3, 14, 0, 0, 0); + } + + public function testCreateFromFormatWithExplicitBangModifier() + { + // Explicit ! should still work + $d = Chronos::createFromFormat('!Y-m-d', '2024-03-14'); + $this->assertDateTime($d, 2024, 3, 14, 0, 0, 0); + } + public function testCreateFromFormatWithTimezoneString() { $d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11', 'Europe/London'); @@ -49,6 +92,18 @@ public function testCreateFromFormatWithMillis() $this->assertSame(254687, $d->micro); } + public function testCreateFromFormatWithUnixTimestamp() + { + $d = Chronos::createFromFormat('U', '0'); + $this->assertDateTime($d, 1970, 1, 1, 0, 0, 0); + } + + public function testCreateFromFormatWithNegativeUnixTimestamp() + { + $d = Chronos::createFromFormat('U', '-1000'); + $this->assertDateTime($d, 1969, 12, 31, 23, 43, 20); + } + public function testCreateFromFormatInvalidFormat() { $parseException = null; @@ -65,9 +120,9 @@ public function testCreateFromFormatInvalidFormat() public function testCreateFromFormatDoesNotUseTestNow() { - // createFromFormat should not use testNow - it should behave like PHP's native method + // testNow should not affect createFromFormat - missing components are zero Chronos::setTestNow(new Chronos('2020-12-01 14:30:45')); - $d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11'); - $this->assertDateTime($d, 1975, 5, 21, 22, 32, 11); + $d = Chronos::createFromFormat('Y-m-d', '2024-03-14'); + $this->assertDateTime($d, 2024, 3, 14, 0, 0, 0); } }