Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/Chronos.php
Original file line number Diff line number Diff line change
Expand Up @@ -660,9 +660,96 @@ public static function createFromFormat(
throw new InvalidArgumentException($message);
}

$testNow = static::getTestNow();
if ($testNow !== null) {
$dateTime = static::applyTestNowToMissingComponents($dateTime, $format, $testNow);
}

return $dateTime;
}

/**
* Apply testNow values to date/time components that weren't in the format string.
*
* @param static $dateTime The parsed datetime instance.
* @param string $format The format string used for parsing.
* @param \Cake\Chronos\Chronos $testNow The test now instance.
* @return static
*/
protected static function applyTestNowToMissingComponents(
self $dateTime,
string $format,
Chronos $testNow,
): static {
// Parse format string to find which characters are actual format specifiers (not escaped)
$formatChars = static::getFormatCharacters($format);

// Check which components are present in the format
$hasYear = (bool)array_intersect($formatChars, ['Y', 'y', 'o', 'X', 'x']);
$hasMonth = (bool)array_intersect($formatChars, ['m', 'n', 'M', 'F']);
$hasDay = (bool)array_intersect($formatChars, ['d', 'j', 'D', 'l', 'N', 'z', 'w', 'W', 'S']);
$hasHour = (bool)array_intersect($formatChars, ['H', 'G', 'h', 'g']);
$hasMinute = (bool)array_intersect($formatChars, ['i']);
$hasSecond = (bool)array_intersect($formatChars, ['s']);
$hasMicro = (bool)array_intersect($formatChars, ['u', 'v']);

// If the format includes '!' or '|', PHP resets unspecified components to Unix epoch or zero
// In that case, we should not override with testNow
$hasReset = in_array('!', $formatChars, true) || in_array('|', $formatChars, true);
if ($hasReset) {
return $dateTime;
}

// Replace missing components with testNow values
$year = $hasYear ? $dateTime->year : $testNow->year;
$month = $hasMonth ? $dateTime->month : $testNow->month;
$day = $hasDay ? $dateTime->day : $testNow->day;
$hour = $hasHour ? $dateTime->hour : $testNow->hour;
$minute = $hasMinute ? $dateTime->minute : $testNow->minute;
$second = $hasSecond ? $dateTime->second : $testNow->second;
$micro = $hasMicro ? $dateTime->micro : $testNow->micro;

// Only modify if something needs to change
if (
!$hasYear || !$hasMonth || !$hasDay ||
!$hasHour || !$hasMinute || !$hasSecond || !$hasMicro
) {
return $dateTime
->setDate($year, $month, $day)
->setTime($hour, $minute, $second, $micro);
}

return $dateTime;
}

/**
* Extract format characters from a format string, handling escapes.
*
* @param string $format The format string.
* @return array<string> Array of format characters.
*/
protected static function getFormatCharacters(string $format): array
{
$chars = [];
$length = strlen($format);
$i = 0;

while ($i < $length) {
$char = $format[$i];

// Backslash escapes the next character
if ($char === '\\' && $i + 1 < $length) {
$i += 2;
continue;
}

$chars[] = $char;
$i++;
}

return $chars;
}

/**
* Returns parse warnings and errors from the last ``createFromFormat()``
* call.
Expand Down
82 changes: 82 additions & 0 deletions tests/TestCase/DateTime/CreateFromFormatTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,88 @@ public function testCreateFromFormatReturnsInstance()
$this->assertTrue($d instanceof Chronos);
}

public function testCreateFromFormatWithTestNowMissingYear()
{
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
$d = Chronos::createFromFormat('m-d H:i:s', '10-05 09:15:30');
$this->assertDateTime($d, 2020, 10, 5, 9, 15, 30);
}

public function testCreateFromFormatWithTestNowMissingDate()
{
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
$d = Chronos::createFromFormat('H:i:s', '09:15:30');
$this->assertDateTime($d, 2020, 12, 1, 9, 15, 30);
}

public function testCreateFromFormatWithTestNowMissingTime()
{
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
$d = Chronos::createFromFormat('Y-m-d', '2021-06-15');
$this->assertDateTime($d, 2021, 6, 15, 14, 30, 45);
}

public function testCreateFromFormatWithTestNowPartialDate()
{
Chronos::setTestNow(new Chronos('2020-12-01 00:00:00'));
$d = Chronos::createFromFormat('m-d', '10-05');
$this->assertDateTime($d, 2020, 10, 5, 0, 0, 0);
}

public function testCreateFromFormatWithTestNowDayOnly()
{
Chronos::setTestNow(new Chronos('2020-12-01 00:00:00'));
$d = Chronos::createFromFormat('d', '05');
$this->assertDateTime($d, 2020, 12, 5, 0, 0, 0);
}

public function testCreateFromFormatWithTestNowComplete()
{
// When format is complete, testNow should not affect the result
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);
}

public function testCreateFromFormatWithTestNowResetModifier()
{
// The '!' modifier resets to Unix epoch, should not use testNow
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
$d = Chronos::createFromFormat('!Y-m-d', '2021-06-15');
$this->assertDateTime($d, 2021, 6, 15, 0, 0, 0);
}

public function testCreateFromFormatWithTestNowPipeModifier()
{
// The '|' modifier resets unspecified components to zero, should not use testNow
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
$d = Chronos::createFromFormat('Y-m-d|', '2021-06-15');
$this->assertDateTime($d, 2021, 6, 15, 0, 0, 0);
}

public function testCreateFromFormatWithoutTestNow()
{
// Without testNow set, behavior should use real current time for missing components
Chronos::setTestNow(null);
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11');
$this->assertDateTime($d, 1975, 5, 21, 22, 32, 11);
}

public function testCreateFromFormatWithTestNowEscapedCharacters()
{
// Escaped format characters should not be treated as format specifiers
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
$d = Chronos::createFromFormat('\Y\-m-d', 'Y-10-05');
$this->assertDateTime($d, 2020, 10, 5, 14, 30, 45);
}

public function testCreateFromFormatWithTestNowMicroseconds()
{
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45.123456'));
$d = Chronos::createFromFormat('Y-m-d H:i:s', '2021-06-15 09:15:30');
$this->assertSame(123456, $d->micro);
}

public function testCreateFromFormatWithTimezoneString()
{
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11', 'Europe/London');
Expand Down