Skip to content

Commit 31aa950

Browse files
authored
Auto-append pipe modifier in createFromFormat for deterministic behavior (#504)
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.
1 parent 59f2c99 commit 31aa950

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

src/Chronos.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,12 @@ public static function createFromTime(
671671
/**
672672
* Create an instance from a specific format
673673
*
674+
* Unlike PHP's native DateTimeImmutable::createFromFormat(), this method
675+
* automatically appends the `|` modifier if no reset modifier (`|` or `!`)
676+
* is present. This ensures that unparsed components are reset to zero
677+
* instead of being filled from the current time, providing more predictable
678+
* and deterministic behavior.
679+
*
674680
* @param string $format The date() compatible format string.
675681
* @param string $time The formatted date string to interpret.
676682
* @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name the new instance should use.
@@ -682,6 +688,12 @@ public static function createFromFormat(
682688
string $time,
683689
DateTimeZone|string|null $timezone = null,
684690
): static {
691+
// Auto-append | modifier if no reset modifier is present
692+
// This ensures unparsed components are zero instead of current time
693+
if (!str_contains($format, '|') && !str_contains($format, '!')) {
694+
$format .= '|';
695+
}
696+
685697
if ($timezone !== null) {
686698
$dateTime = parent::createFromFormat($format, $time, $timezone ? static::safeCreateDateTimeZone($timezone) : null);
687699
} else {

tests/TestCase/DateTime/CreateFromFormatTest.php

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

32+
public function testCreateFromFormatMissingTimeIsZero()
33+
{
34+
// Missing time components should be zero, not current time
35+
$d = Chronos::createFromFormat('Y-m-d', '2024-03-14');
36+
$this->assertDateTime($d, 2024, 3, 14, 0, 0, 0);
37+
$this->assertSame(0, $d->micro);
38+
}
39+
40+
public function testCreateFromFormatMissingSecondsIsZero()
41+
{
42+
// Missing seconds should be zero
43+
$d = Chronos::createFromFormat('Y-m-d H:i', '2024-03-14 12:30');
44+
$this->assertDateTime($d, 2024, 3, 14, 12, 30, 0);
45+
}
46+
47+
public function testCreateFromFormatMissingDateIsEpoch()
48+
{
49+
// Missing date components should be Unix epoch (1970-01-01)
50+
$d = Chronos::createFromFormat('H:i:s', '12:30:45');
51+
$this->assertDateTime($d, 1970, 1, 1, 12, 30, 45);
52+
}
53+
54+
public function testCreateFromFormatMissingMicrosecondsIsZero()
55+
{
56+
// Missing microseconds should be zero
57+
$d = Chronos::createFromFormat('Y-m-d H:i:s', '2024-03-14 12:30:45');
58+
$this->assertSame(0, $d->micro);
59+
}
60+
61+
public function testCreateFromFormatWithExplicitPipeModifier()
62+
{
63+
// Explicit | should still work
64+
$d = Chronos::createFromFormat('Y-m-d|', '2024-03-14');
65+
$this->assertDateTime($d, 2024, 3, 14, 0, 0, 0);
66+
}
67+
68+
public function testCreateFromFormatWithExplicitBangModifier()
69+
{
70+
// Explicit ! should still work
71+
$d = Chronos::createFromFormat('!Y-m-d', '2024-03-14');
72+
$this->assertDateTime($d, 2024, 3, 14, 0, 0, 0);
73+
}
74+
3275
public function testCreateFromFormatWithTimezoneString()
3376
{
3477
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11', 'Europe/London');
@@ -49,6 +92,18 @@ public function testCreateFromFormatWithMillis()
4992
$this->assertSame(254687, $d->micro);
5093
}
5194

95+
public function testCreateFromFormatWithUnixTimestamp()
96+
{
97+
$d = Chronos::createFromFormat('U', '0');
98+
$this->assertDateTime($d, 1970, 1, 1, 0, 0, 0);
99+
}
100+
101+
public function testCreateFromFormatWithNegativeUnixTimestamp()
102+
{
103+
$d = Chronos::createFromFormat('U', '-1000');
104+
$this->assertDateTime($d, 1969, 12, 31, 23, 43, 20);
105+
}
106+
52107
public function testCreateFromFormatInvalidFormat()
53108
{
54109
$parseException = null;
@@ -65,9 +120,9 @@ public function testCreateFromFormatInvalidFormat()
65120

66121
public function testCreateFromFormatDoesNotUseTestNow()
67122
{
68-
// createFromFormat should not use testNow - it should behave like PHP's native method
123+
// testNow should not affect createFromFormat - missing components are zero
69124
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);
125+
$d = Chronos::createFromFormat('Y-m-d', '2024-03-14');
126+
$this->assertDateTime($d, 2024, 3, 14, 0, 0, 0);
72127
}
73128
}

0 commit comments

Comments
 (0)