diff --git a/src/ZonedDateTime.php b/src/ZonedDateTime.php index 92eebb10..28f2a822 100644 --- a/src/ZonedDateTime.php +++ b/src/ZonedDateTime.php @@ -19,6 +19,7 @@ use function assert; use function intdiv; use function rtrim; +use function sprintf; use function str_pad; use const STR_PAD_LEFT; @@ -166,14 +167,34 @@ public static function from(DateTimeParseResult $result): ZonedDateTime if ($result->hasField(Field\TimeZoneRegion::NAME)) { $timeZone = TimeZoneRegion::from($result); - } else { - $timeZone = $timeZoneOffset; + + $zonedDateTime = ZonedDateTime::ofInstant( + ZonedDateTime::of($localDateTime, $timeZoneOffset)->getInstant(), + $timeZone, + ); + + if (! $zonedDateTime->getTimeZoneOffset()->isEqualTo($timeZoneOffset)) { + // In a gap, of() shifts the local date-time forward. + if (! ZonedDateTime::of($localDateTime, $timeZone)->getDateTime()->isEqualTo($localDateTime)) { + throw new DateTimeException(sprintf( + 'The local date-time %s does not exist in %s.', + $localDateTime, + $timeZone, + )); + } + + throw new DateTimeException(sprintf( + 'The offset %s is not valid for %s in %s.', + $timeZoneOffset, + $localDateTime, + $timeZone, + )); + } + + return $zonedDateTime; } - return ZonedDateTime::of( - $localDateTime, - $timeZone, - ); + return ZonedDateTime::of($localDateTime, $timeZoneOffset); } /** diff --git a/tests/ZonedDateTimeTest.php b/tests/ZonedDateTimeTest.php index e88b7cb0..2090a222 100644 --- a/tests/ZonedDateTimeTest.php +++ b/tests/ZonedDateTimeTest.php @@ -482,6 +482,9 @@ public static function providerParse(): iterable ['2001-02-03T01:02+00:00[Europe/London]', '2001-02-03', '01:02', 'Z', 'Europe/London'], ['2001-02-03T01:02:03-00:00[Europe/London]', '2001-02-03', '01:02:03', 'Z', 'Europe/London'], ['2001-02-03T01:02:03.456+00:00[Europe/London]', '2001-02-03', '01:02:03.456', 'Z', 'Europe/London'], + ['2001-07-03T01:02+01:00[Europe/London]', '2001-07-03', '01:02', '+01:00', 'Europe/London'], + ['2024-10-27T02:30+02:00[Europe/Zurich]', '2024-10-27', '02:30', '+02:00', 'Europe/Zurich'], + ['2024-10-27T02:30+01:00[Europe/Zurich]', '2024-10-27', '02:30', '+01:00', 'Europe/Zurich'], ]; if (PHP_VERSION_ID >= 80107) { @@ -489,6 +492,26 @@ public static function providerParse(): iterable } } + #[DataProvider('providerParseWithOffsetNotValidForRegionThrowsException')] + public function testParseWithOffsetNotValidForRegionThrowsException(string $text, string $expectedMessage): void + { + $this->expectException(DateTimeException::class); + $this->expectExceptionMessage($expectedMessage); + + ZonedDateTime::parse($text); + } + + public static function providerParseWithOffsetNotValidForRegionThrowsException(): array + { + return [ + ['2001-02-03T01:02+01:00[Europe/London]', 'The offset +01:00 is not valid for 2001-02-03T01:02 in Europe/London.'], + ['2024-06-01T12:00Z[Europe/Zurich]', 'The offset Z is not valid for 2024-06-01T12:00 in Europe/Zurich.'], + ['2024-06-01T12:00+05:00[Europe/Zurich]', 'The offset +05:00 is not valid for 2024-06-01T12:00 in Europe/Zurich.'], + ['2024-03-31T02:30+01:00[Europe/Zurich]', 'The local date-time 2024-03-31T02:30 does not exist in Europe/Zurich.'], + ['2024-03-31T02:30+02:00[Europe/Zurich]', 'The local date-time 2024-03-31T02:30 does not exist in Europe/Zurich.'], + ]; + } + #[DataProvider('providerParseInvalidStringThrowsException')] public function testParseInvalidStringThrowsException(string $text): void { @@ -758,7 +781,7 @@ public static function providerPlusPeriod(): Generator ['2000-02-16T12:34:56.123456789-08:00[America/Los_Angeles]', Period::ofMonths(2), '2000-04-16T12:34:56.123456789-07:00[America/Los_Angeles]'], ['2000-05-16T12:34:56.123456789-07:00[America/Los_Angeles]', Period::ofMonths(-1), '2000-04-16T12:34:56.123456789-07:00[America/Los_Angeles]'], ['2000-05-16T12:34:56.123456789-07:00[America/Los_Angeles]', Period::ofMonths(-2), '2000-03-16T12:34:56.123456789-08:00[America/Los_Angeles]'], - ['2000-02-16T12:34:56.123456789-07:00[America/Los_Angeles]', Period::of(1, -2, 3), '2000-12-19T12:34:56.123456789-08:00[America/Los_Angeles]'], + ['2000-02-16T12:34:56.123456789-08:00[America/Los_Angeles]', Period::of(1, -2, 3), '2000-12-19T12:34:56.123456789-08:00[America/Los_Angeles]'], ]; foreach (self::providerPlusDays() as [$zonedDateTime, $plusDays, $expected]) {