Skip to content
Merged
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
37 changes: 37 additions & 0 deletions inc/Blocks/Calendar/Grouping/MultiDayResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ public static function is_multi_day( array $event_data ): bool {
return false;
}

// Placeholder / TBD dates (e.g. "2026-07-??") are not parseable and
// cannot be expanded into a date range. Treat them as single-day so
// the calendar still renders rather than throwing a fatal.
if ( ! self::is_valid_date( $start_date ) || ! self::is_valid_date( $end_date ) ) {
return false;
}

$start = new DateTime( $start_date );
$end = new DateTime( $end_date );
$diff = $start->diff( $end )->days;
Expand Down Expand Up @@ -75,6 +82,17 @@ public static function is_multi_day( array $event_data ): bool {
public static function get_date_range( string $start_date, string $end_date, DateTimeZone $event_tz ): array {
$dates = array();

// Guard against placeholder / malformed dates that would throw a
// DateMalformedStringException. is_multi_day() already filters these,
// but get_date_range() is a public entry point in its own right.
if ( ! self::is_valid_date( $start_date ) || ! self::is_valid_date( $end_date ) ) {
if ( self::is_valid_date( $start_date ) ) {
$dates[] = $start_date;
}

return $dates;
}

$start = new DateTime( $start_date, $event_tz );
$end = new DateTime( $end_date, $event_tz );

Expand All @@ -89,4 +107,23 @@ public static function get_date_range( string $start_date, string $end_date, Dat

return $dates;
}

/**
* Strictly validate a Y-m-d date string.
*
* Rejects placeholder / TBD values (e.g. "2026-07-??") and any string
* that does not represent a real calendar date, so callers can avoid
* passing unparseable input into DateTime and triggering a
* DateMalformedStringException.
*
* @param string $date Date string to validate.
* @return bool True when $date is a valid Y-m-d calendar date.
*/
private static function is_valid_date( string $date ): bool {
if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches ) ) {
return false;
}

return checkdate( (int) $matches[2], (int) $matches[3], (int) $matches[1] );
}
}
105 changes: 105 additions & 0 deletions tests/Unit/MultiDayResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php
/**
* MultiDayResolver Tests
*
* Guards against malformed / placeholder dates (e.g. "2026-07-??") that
* previously reached `new DateTime()` and threw a fatal
* DateMalformedStringException.
*
* @package DataMachineEvents\Tests\Unit
* @since 0.44.1
*/

namespace DataMachineEvents\Tests\Unit;

use WP_UnitTestCase;
use DateTimeZone;
use DataMachineEvents\Blocks\Calendar\Grouping\MultiDayResolver;

class MultiDayResolverTest extends WP_UnitTestCase {

/**
* Regression: a placeholder end date ("2026-07-??") must not throw.
*
* Previously `is_multi_day()` passed the raw string straight into
* `new DateTime()`, which throws DateMalformedStringException on PHP 8.3+.
*/
public function test_is_multi_day_handles_placeholder_end_date_without_throwing() {
$result = MultiDayResolver::is_multi_day(
array(
'startDate' => '2026-07-01',
'endDate' => '2026-07-??',
)
);

$this->assertFalse( $result, 'Placeholder dates cannot form a range and must be treated as single-day.' );
}

public function test_is_multi_day_handles_placeholder_start_date_without_throwing() {
$result = MultiDayResolver::is_multi_day(
array(
'startDate' => '2026-??-??',
'endDate' => '2026-07-05',
)
);

$this->assertFalse( $result );
}

public function test_is_multi_day_returns_false_for_invalid_calendar_dates() {
$result = MultiDayResolver::is_multi_day(
array(
'startDate' => '2026-02-30',
'endDate' => '2026-03-05',
)
);

$this->assertFalse( $result );
}

public function test_is_multi_day_true_for_genuine_multi_day_span() {
$result = MultiDayResolver::is_multi_day(
array(
'startDate' => '2026-07-01',
'endDate' => '2026-07-05',
)
);

$this->assertTrue( $result );
}

public function test_is_multi_day_false_for_same_day() {
$result = MultiDayResolver::is_multi_day(
array(
'startDate' => '2026-07-01',
'endDate' => '2026-07-01',
)
);

$this->assertFalse( $result );
}

public function test_get_date_range_returns_empty_for_malformed_dates() {
$tz = new DateTimeZone( 'America/New_York' );
$range = MultiDayResolver::get_date_range( '2026-07-??', '2026-07-??', $tz );

$this->assertSame( array(), $range );
}

public function test_get_date_range_returns_single_start_when_end_is_malformed() {
$tz = new DateTimeZone( 'America/New_York' );
$range = MultiDayResolver::get_date_range( '2026-07-01', '2026-07-??', $tz );

$this->assertSame( array( '2026-07-01' ), $range );
}

public function test_get_date_range_expands_valid_span() {
$tz = new DateTimeZone( 'America/New_York' );
$range = MultiDayResolver::get_date_range( '2026-07-01', '2026-07-03', $tz );

$this->assertSame(
array( '2026-07-01', '2026-07-02', '2026-07-03' ),
$range
);
}
}
Loading