Skip to content

Commit 960e7ec

Browse files
authored
Revert testNow behavior in createFromFormat for BC (#503)
The change in 3.3.2 that made createFromFormat respect testNow for missing components was a BC break. This reverts that behavior. createFromFormat now behaves exactly like PHP's native DateTimeImmutable::createFromFormat - testNow has no effect. The testNow feature for createFromFormat may be re-added as opt-in in a future major version.
1 parent 77a248b commit 960e7ec

File tree

2 files changed

+8
-187
lines changed

2 files changed

+8
-187
lines changed

src/Chronos.php

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -660,98 +660,9 @@ public static function createFromFormat(
660660
throw new InvalidArgumentException($message);
661661
}
662662

663-
$testNow = static::getTestNow();
664-
if ($testNow !== null) {
665-
$dateTime = static::applyTestNowToMissingComponents($dateTime, $format, $testNow);
666-
}
667-
668663
return $dateTime;
669664
}
670665

671-
/**
672-
* Apply testNow values to date/time components that weren't in the format string.
673-
*
674-
* @param static $dateTime The parsed datetime instance.
675-
* @param string $format The format string used for parsing.
676-
* @param \Cake\Chronos\Chronos $testNow The test now instance.
677-
* @return static
678-
*/
679-
protected static function applyTestNowToMissingComponents(
680-
self $dateTime,
681-
string $format,
682-
Chronos $testNow,
683-
): static {
684-
// Parse format string to find which characters are actual format specifiers (not escaped)
685-
$formatChars = static::getFormatCharacters($format);
686-
687-
// Check which components are present in the format
688-
$hasYear = (bool)array_intersect($formatChars, ['Y', 'y', 'o', 'X', 'x']);
689-
$hasMonth = (bool)array_intersect($formatChars, ['m', 'n', 'M', 'F']);
690-
$hasDay = (bool)array_intersect($formatChars, ['d', 'j', 'D', 'l', 'N', 'z', 'w', 'W', 'S']);
691-
$hasHour = (bool)array_intersect($formatChars, ['H', 'G', 'h', 'g']);
692-
$hasMinute = (bool)array_intersect($formatChars, ['i']);
693-
$hasSecond = (bool)array_intersect($formatChars, ['s']);
694-
$hasMicro = (bool)array_intersect($formatChars, ['u', 'v']);
695-
696-
// If the format includes '!' or '|', PHP resets unspecified components to Unix epoch or zero
697-
// If 'U' is present, all components are set from the Unix timestamp
698-
// In these cases, we should not override with testNow
699-
$hasReset = in_array('!', $formatChars, true) || in_array('|', $formatChars, true);
700-
$hasUnixTimestamp = in_array('U', $formatChars, true);
701-
if ($hasReset || $hasUnixTimestamp) {
702-
return $dateTime;
703-
}
704-
705-
// Replace missing components with testNow values
706-
$year = $hasYear ? $dateTime->year : $testNow->year;
707-
$month = $hasMonth ? $dateTime->month : $testNow->month;
708-
$day = $hasDay ? $dateTime->day : $testNow->day;
709-
$hour = $hasHour ? $dateTime->hour : $testNow->hour;
710-
$minute = $hasMinute ? $dateTime->minute : $testNow->minute;
711-
$second = $hasSecond ? $dateTime->second : $testNow->second;
712-
$micro = $hasMicro ? $dateTime->micro : $testNow->micro;
713-
714-
// Only modify if something needs to change
715-
if (
716-
!$hasYear || !$hasMonth || !$hasDay ||
717-
!$hasHour || !$hasMinute || !$hasSecond || !$hasMicro
718-
) {
719-
return $dateTime
720-
->setDate($year, $month, $day)
721-
->setTime($hour, $minute, $second, $micro);
722-
}
723-
724-
return $dateTime;
725-
}
726-
727-
/**
728-
* Extract format characters from a format string, handling escapes.
729-
*
730-
* @param string $format The format string.
731-
* @return array<string> Array of format characters.
732-
*/
733-
protected static function getFormatCharacters(string $format): array
734-
{
735-
$chars = [];
736-
$length = strlen($format);
737-
$i = 0;
738-
739-
while ($i < $length) {
740-
$char = $format[$i];
741-
742-
// Backslash escapes the next character
743-
if ($char === '\\' && $i + 1 < $length) {
744-
$i += 2;
745-
continue;
746-
}
747-
748-
$chars[] = $char;
749-
$i++;
750-
}
751-
752-
return $chars;
753-
}
754-
755666
/**
756667
* Returns parse warnings and errors from the last ``createFromFormat()``
757668
* call.

tests/TestCase/DateTime/CreateFromFormatTest.php

Lines changed: 8 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -29,104 +29,6 @@ public function testCreateFromFormatReturnsInstance()
2929
$this->assertTrue($d instanceof Chronos);
3030
}
3131

32-
public function testCreateFromFormatWithTestNowMissingYear()
33-
{
34-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
35-
$d = Chronos::createFromFormat('m-d H:i:s', '10-05 09:15:30');
36-
$this->assertDateTime($d, 2020, 10, 5, 9, 15, 30);
37-
}
38-
39-
public function testCreateFromFormatWithTestNowMissingDate()
40-
{
41-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
42-
$d = Chronos::createFromFormat('H:i:s', '09:15:30');
43-
$this->assertDateTime($d, 2020, 12, 1, 9, 15, 30);
44-
}
45-
46-
public function testCreateFromFormatWithTestNowMissingTime()
47-
{
48-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
49-
$d = Chronos::createFromFormat('Y-m-d', '2021-06-15');
50-
$this->assertDateTime($d, 2021, 6, 15, 14, 30, 45);
51-
}
52-
53-
public function testCreateFromFormatWithTestNowPartialDate()
54-
{
55-
Chronos::setTestNow(new Chronos('2020-12-01 00:00:00'));
56-
$d = Chronos::createFromFormat('m-d', '10-05');
57-
$this->assertDateTime($d, 2020, 10, 5, 0, 0, 0);
58-
}
59-
60-
public function testCreateFromFormatWithTestNowDayOnly()
61-
{
62-
Chronos::setTestNow(new Chronos('2020-12-01 00:00:00'));
63-
$d = Chronos::createFromFormat('d', '05');
64-
$this->assertDateTime($d, 2020, 12, 5, 0, 0, 0);
65-
}
66-
67-
public function testCreateFromFormatWithTestNowComplete()
68-
{
69-
// When format is complete, testNow should not affect the result
70-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
71-
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11');
72-
$this->assertDateTime($d, 1975, 5, 21, 22, 32, 11);
73-
}
74-
75-
public function testCreateFromFormatWithTestNowResetModifier()
76-
{
77-
// The '!' modifier resets to Unix epoch, should not use testNow
78-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
79-
$d = Chronos::createFromFormat('!Y-m-d', '2021-06-15');
80-
$this->assertDateTime($d, 2021, 6, 15, 0, 0, 0);
81-
}
82-
83-
public function testCreateFromFormatWithTestNowPipeModifier()
84-
{
85-
// The '|' modifier resets unspecified components to zero, should not use testNow
86-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
87-
$d = Chronos::createFromFormat('Y-m-d|', '2021-06-15');
88-
$this->assertDateTime($d, 2021, 6, 15, 0, 0, 0);
89-
}
90-
91-
public function testCreateFromFormatWithoutTestNow()
92-
{
93-
// Without testNow set, behavior should use real current time for missing components
94-
Chronos::setTestNow(null);
95-
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11');
96-
$this->assertDateTime($d, 1975, 5, 21, 22, 32, 11);
97-
}
98-
99-
public function testCreateFromFormatWithTestNowEscapedCharacters()
100-
{
101-
// Escaped format characters should not be treated as format specifiers
102-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
103-
$d = Chronos::createFromFormat('\Y\-m-d', 'Y-10-05');
104-
$this->assertDateTime($d, 2020, 10, 5, 14, 30, 45);
105-
}
106-
107-
public function testCreateFromFormatWithTestNowMicroseconds()
108-
{
109-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45.123456'));
110-
$d = Chronos::createFromFormat('Y-m-d H:i:s', '2021-06-15 09:15:30');
111-
$this->assertSame(123456, $d->micro);
112-
}
113-
114-
public function testCreateFromFormatWithTestNowUnixTimestamp()
115-
{
116-
// Unix timestamp ('U' format) sets all components, should not use testNow
117-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
118-
$d = Chronos::createFromFormat('U', '0');
119-
$this->assertDateTime($d, 1970, 1, 1, 0, 0, 0);
120-
}
121-
122-
public function testCreateFromFormatWithTestNowNegativeUnixTimestamp()
123-
{
124-
// Negative Unix timestamp should also not use testNow
125-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
126-
$d = Chronos::createFromFormat('U', '-1000');
127-
$this->assertDateTime($d, 1969, 12, 31, 23, 43, 20);
128-
}
129-
13032
public function testCreateFromFormatWithTimezoneString()
13133
{
13234
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11', 'Europe/London');
@@ -160,4 +62,12 @@ public function testCreateFromFormatInvalidFormat()
16062
$this->assertIsArray(Chronos::getLastErrors());
16163
$this->assertNotEmpty(Chronos::getLastErrors()['errors']);
16264
}
65+
66+
public function testCreateFromFormatDoesNotUseTestNow()
67+
{
68+
// createFromFormat should not use testNow - it should behave like PHP's native method
69+
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
70+
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11');
71+
$this->assertDateTime($d, 1975, 5, 21, 22, 32, 11);
72+
}
16373
}

0 commit comments

Comments
 (0)