Skip to content

Commit 90fbb59

Browse files
authored
Merge pull request #494 from cakephp/fix-createfromformat-testnow
Fix createFromFormat to respect setTestNow for missing components
2 parents 295b8c6 + 10c5247 commit 90fbb59

2 files changed

Lines changed: 169 additions & 0 deletions

File tree

src/Chronos.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,9 +660,96 @@ 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+
663668
return $dateTime;
664669
}
665670

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+
// In that case, we should not override with testNow
698+
$hasReset = in_array('!', $formatChars, true) || in_array('|', $formatChars, true);
699+
if ($hasReset) {
700+
return $dateTime;
701+
}
702+
703+
// Replace missing components with testNow values
704+
$year = $hasYear ? $dateTime->year : $testNow->year;
705+
$month = $hasMonth ? $dateTime->month : $testNow->month;
706+
$day = $hasDay ? $dateTime->day : $testNow->day;
707+
$hour = $hasHour ? $dateTime->hour : $testNow->hour;
708+
$minute = $hasMinute ? $dateTime->minute : $testNow->minute;
709+
$second = $hasSecond ? $dateTime->second : $testNow->second;
710+
$micro = $hasMicro ? $dateTime->micro : $testNow->micro;
711+
712+
// Only modify if something needs to change
713+
if (
714+
!$hasYear || !$hasMonth || !$hasDay ||
715+
!$hasHour || !$hasMinute || !$hasSecond || !$hasMicro
716+
) {
717+
return $dateTime
718+
->setDate($year, $month, $day)
719+
->setTime($hour, $minute, $second, $micro);
720+
}
721+
722+
return $dateTime;
723+
}
724+
725+
/**
726+
* Extract format characters from a format string, handling escapes.
727+
*
728+
* @param string $format The format string.
729+
* @return array<string> Array of format characters.
730+
*/
731+
protected static function getFormatCharacters(string $format): array
732+
{
733+
$chars = [];
734+
$length = strlen($format);
735+
$i = 0;
736+
737+
while ($i < $length) {
738+
$char = $format[$i];
739+
740+
// Backslash escapes the next character
741+
if ($char === '\\' && $i + 1 < $length) {
742+
$i += 2;
743+
continue;
744+
}
745+
746+
$chars[] = $char;
747+
$i++;
748+
}
749+
750+
return $chars;
751+
}
752+
666753
/**
667754
* Returns parse warnings and errors from the last ``createFromFormat()``
668755
* call.

tests/TestCase/DateTime/CreateFromFormatTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,88 @@ 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+
32114
public function testCreateFromFormatWithTimezoneString()
33115
{
34116
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11', 'Europe/London');

0 commit comments

Comments
 (0)