From de030fe09a0138477e3a85febd50980148e9c946 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Mon, 13 Oct 2025 10:13:23 +0300 Subject: [PATCH 1/6] Allow to parse TimeZoneOffset with hours only --- src/Parser/IsoParsers.php | 2 ++ src/TimeZoneOffset.php | 2 +- tests/LocalDateTimeTest.php | 6 ++++++ tests/TimeZoneOffsetTest.php | 10 +++++++++- tests/ZonedDateTimeTest.php | 25 +++++++++++++++++++++++++ 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Parser/IsoParsers.php b/src/Parser/IsoParsers.php index d4545dd..04922b2 100644 --- a/src/Parser/IsoParsers.php +++ b/src/Parser/IsoParsers.php @@ -220,8 +220,10 @@ public static function timeZoneOffset(): PatternParser ->startGroup() ->appendCapturePattern('[\-\+]', TimeZoneOffsetSign::NAME) ->appendCapturePattern(TimeZoneOffsetHour::PATTERN, TimeZoneOffsetHour::NAME) + ->startOptional() ->appendLiteral(':') ->appendCapturePattern(TimeZoneOffsetMinute::PATTERN, TimeZoneOffsetMinute::NAME) + ->endOptional() ->startOptional() ->appendLiteral(':') ->appendCapturePattern(TimeZoneOffsetSecond::PATTERN, TimeZoneOffsetSecond::NAME) diff --git a/src/TimeZoneOffset.php b/src/TimeZoneOffset.php index a9ac434..60637b4 100644 --- a/src/TimeZoneOffset.php +++ b/src/TimeZoneOffset.php @@ -106,7 +106,7 @@ public static function from(DateTimeParseResult $result): TimeZoneOffset } $hour = $result->getField(Field\TimeZoneOffsetHour::NAME); - $minute = $result->getField(Field\TimeZoneOffsetMinute::NAME); + $minute = $result->getOptionalField(Field\TimeZoneOffsetMinute::NAME); $second = $result->getOptionalField(Field\TimeZoneOffsetSecond::NAME); $hour = (int) $hour; diff --git a/tests/LocalDateTimeTest.php b/tests/LocalDateTimeTest.php index 5263aa3..7a9c961 100644 --- a/tests/LocalDateTimeTest.php +++ b/tests/LocalDateTimeTest.php @@ -1059,7 +1059,9 @@ public static function providerAtTimeZone(): array { return [ ['2001-03-28T23:23:23', '-06:00', 985843403, 0], + ['2001-03-28T23:23:23', '-06', 985843403, 0], ['1960-04-30T06:00:00.123456', '+02:00', -305236800, 123456000], + ['1960-04-30T06:00:00.123456', '+02', -305236800, 123456000], ['2008-01-02T12:34:56', 'Europe/Paris', 1199273696, 0], ['2008-01-02T12:34:56.123', 'America/Los_Angeles', 1199306096, 123000000], ]; @@ -1128,9 +1130,13 @@ public static function providerForPastFuture(): array { return [ [1234567890, '2009-02-14T00:31:29', '+01:00', false], + [1234567890, '2009-02-14T00:31:29', '+01', false], [1234567890, '2009-02-14T00:31:31', '+01:00', true], + [1234567890, '2009-02-14T00:31:31', '+01', true], [2345678901, '2044-04-30T17:28:20', '-08:00', false], + [2345678901, '2044-04-30T17:28:20', '-08', false], [2345678901, '2044-04-30T17:28:22', '-08:00', true], + [2345678901, '2044-04-30T17:28:22', '-08', true], ]; } diff --git a/tests/TimeZoneOffsetTest.php b/tests/TimeZoneOffsetTest.php index ad8fb38..f080a4d 100644 --- a/tests/TimeZoneOffsetTest.php +++ b/tests/TimeZoneOffsetTest.php @@ -153,9 +153,13 @@ public static function providerParse(): iterable { yield from [ ['+00:00', 0], + ['+00', 0], ['-00:00', 0], + ['-00', 0], ['+01:00', 3600], + ['+01', 3600], ['-01:00', -3600], + ['-01', -3600], ['+01:30', 5400], ['-01:30', -5400], ['+18:00', 64800], @@ -182,7 +186,6 @@ public static function providerParseInvalidStringThrowsException(): array return [ [''], ['00:00'], - ['+00'], ['+00:'], ['+00:00:'], ['+1:00'], @@ -244,11 +247,13 @@ public static function providerGetId(): iterable [60, '+00:01'], [120, '+00:02'], [3600, '+01:00'], + [3600, '+01'], [7380, '+02:03'], [64800, '+18:00'], [-60, '-00:01'], [-120, '-00:02'], [-3600, '-01:00'], + [-3600, '-01'], [-7380, '-02:03'], [-64800, '-18:00'], ]; @@ -292,6 +297,7 @@ public static function providerToNativeDateTimeZone(): iterable { yield from [ [-18000, '-05:00'], + [-18000, '-05'], ]; if ((PHP_VERSION_ID >= 80107 && PHP_VERSION_ID < 80120) @@ -299,7 +305,9 @@ public static function providerToNativeDateTimeZone(): iterable ) { yield from [ [-1, '-00:00'], + [-1, '-00'], [3630, '+01:00'], + [3630, '+01'], ]; } diff --git a/tests/ZonedDateTimeTest.php b/tests/ZonedDateTimeTest.php index 2382911..ade120a 100644 --- a/tests/ZonedDateTimeTest.php +++ b/tests/ZonedDateTimeTest.php @@ -102,6 +102,30 @@ public static function providerOf(): array ['2011-06-30T12:34:56.123456789', '+17:00', '+17:00', 0, 1309376096, 123456789], ['2011-07-31T12:34:56.123456789', '+18:00', '+18:00', 0, 1312050896, 123456789], + ['2000-01-01T12:34:56.123456789', '-18', '-18:00', 0, 946794896, 123456789], + ['2001-02-02T12:34:56.123456789', '-17', '-17:00', 0, 981178496, 123456789], + ['2002-03-03T12:34:56.123456789', '-16', '-16:00', 0, 1015216496, 123456789], + ['2003-04-04T12:34:56.123456789', '-15', '-15:00', 0, 1049513696, 123456789], + ['2004-05-05T12:34:56.123456789', '-14', '-14:00', 0, 1083810896, 123456789], + ['2005-06-06T12:34:56.123456789', '-06', '-06:00', 0, 1118082896, 123456789], + ['2006-07-07T12:34:56.123456789', '-05', '-05:00', 0, 1152293696, 123456789], + ['2007-08-08T12:34:56.123456789', '-04', '-04:00', 0, 1186590896, 123456789], + ['2008-09-09T12:34:56.123456789', '-03', '-03:00', 0, 1220974496, 123456789], + ['2009-10-10T12:34:56.123456789', '-02', '-02:00', 0, 1255185296, 123456789], + ['2011-12-12T12:34:56.123456789', '-01', '-01:00', 0, 1323696896, 123456789], + ['2015-04-16T12:34:56.123456789', '+00', '+00:00', 0, 1429187696, 123456789], + ['2019-08-20T12:34:56.123456789', '+01', '+01:00', 0, 1566300896, 123456789], + ['2011-10-22T12:34:56.123456789', '+02', '+02:00', 0, 1319279696, 123456789], + ['2011-11-23T12:34:56.123456789', '+03', '+03:00', 0, 1322040896, 123456789], + ['2011-12-24T12:34:56.123456789', '+04', '+04:00', 0, 1324715696, 123456789], + ['2011-01-25T12:34:56.123456789', '+05', '+05:00', 0, 1295940896, 123456789], + ['2011-02-26T12:34:56.123456789', '+06', '+06:00', 0, 1298702096, 123456789], + ['2011-03-27T12:34:56.123456789', '+14', '+14:00', 0, 1301178896, 123456789], + ['2011-04-28T12:34:56.123456789', '+15', '+15:00', 0, 1303940096, 123456789], + ['2011-05-29T12:34:56.123456789', '+16', '+16:00', 0, 1306614896, 123456789], + ['2011-06-30T12:34:56.123456789', '+17', '+17:00', 0, 1309376096, 123456789], + ['2011-07-31T12:34:56.123456789', '+18', '+18:00', 0, 1312050896, 123456789], + // WITH REGION, NORMAL: NOT WITHIN A DST TRANSITION // The region is resolved to an offset without ambiguity. // The date-time is resolved to an instant without ambiguity. @@ -375,6 +399,7 @@ public static function providerParse(): iterable ['2001-02-03T01:02:03.456Z', '2001-02-03', '01:02:03.456', 'Z', 'Z'], ['2001-02-03T01:02-03:00', '2001-02-03', '01:02', '-03:00', '-03:00'], ['2001-02-03T01:02:03+04:00', '2001-02-03', '01:02:03', '+04:00', '+04:00'], + ['2001-02-03T01:02:03+04:00', '2001-02-03', '01:02:03', '+04', '+04:00'], ['2001-02-03T01:02:03.456+12:34', '2001-02-03', '01:02:03.456', '+12:34', '+12:34'], ['2001-02-03T01:02Z[Europe/London]', '2001-02-03', '01:02', 'Z', 'Europe/London'], ['2001-02-03T01:02+00:00[Europe/London]', '2001-02-03', '01:02', 'Z', 'Europe/London'], From e91e913bab91a30dfb947ca325996518e2c54dfd Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Mon, 13 Oct 2025 10:17:09 +0300 Subject: [PATCH 2/6] Allow to parse TimeZoneOffset with hours only --- tests/LocalDateTimeTest.php | 6 ------ tests/TimeZoneOffsetTest.php | 10 +--------- tests/ZonedDateTimeTest.php | 25 ------------------------- 3 files changed, 1 insertion(+), 40 deletions(-) diff --git a/tests/LocalDateTimeTest.php b/tests/LocalDateTimeTest.php index 7a9c961..5263aa3 100644 --- a/tests/LocalDateTimeTest.php +++ b/tests/LocalDateTimeTest.php @@ -1059,9 +1059,7 @@ public static function providerAtTimeZone(): array { return [ ['2001-03-28T23:23:23', '-06:00', 985843403, 0], - ['2001-03-28T23:23:23', '-06', 985843403, 0], ['1960-04-30T06:00:00.123456', '+02:00', -305236800, 123456000], - ['1960-04-30T06:00:00.123456', '+02', -305236800, 123456000], ['2008-01-02T12:34:56', 'Europe/Paris', 1199273696, 0], ['2008-01-02T12:34:56.123', 'America/Los_Angeles', 1199306096, 123000000], ]; @@ -1130,13 +1128,9 @@ public static function providerForPastFuture(): array { return [ [1234567890, '2009-02-14T00:31:29', '+01:00', false], - [1234567890, '2009-02-14T00:31:29', '+01', false], [1234567890, '2009-02-14T00:31:31', '+01:00', true], - [1234567890, '2009-02-14T00:31:31', '+01', true], [2345678901, '2044-04-30T17:28:20', '-08:00', false], - [2345678901, '2044-04-30T17:28:20', '-08', false], [2345678901, '2044-04-30T17:28:22', '-08:00', true], - [2345678901, '2044-04-30T17:28:22', '-08', true], ]; } diff --git a/tests/TimeZoneOffsetTest.php b/tests/TimeZoneOffsetTest.php index f080a4d..ad8fb38 100644 --- a/tests/TimeZoneOffsetTest.php +++ b/tests/TimeZoneOffsetTest.php @@ -153,13 +153,9 @@ public static function providerParse(): iterable { yield from [ ['+00:00', 0], - ['+00', 0], ['-00:00', 0], - ['-00', 0], ['+01:00', 3600], - ['+01', 3600], ['-01:00', -3600], - ['-01', -3600], ['+01:30', 5400], ['-01:30', -5400], ['+18:00', 64800], @@ -186,6 +182,7 @@ public static function providerParseInvalidStringThrowsException(): array return [ [''], ['00:00'], + ['+00'], ['+00:'], ['+00:00:'], ['+1:00'], @@ -247,13 +244,11 @@ public static function providerGetId(): iterable [60, '+00:01'], [120, '+00:02'], [3600, '+01:00'], - [3600, '+01'], [7380, '+02:03'], [64800, '+18:00'], [-60, '-00:01'], [-120, '-00:02'], [-3600, '-01:00'], - [-3600, '-01'], [-7380, '-02:03'], [-64800, '-18:00'], ]; @@ -297,7 +292,6 @@ public static function providerToNativeDateTimeZone(): iterable { yield from [ [-18000, '-05:00'], - [-18000, '-05'], ]; if ((PHP_VERSION_ID >= 80107 && PHP_VERSION_ID < 80120) @@ -305,9 +299,7 @@ public static function providerToNativeDateTimeZone(): iterable ) { yield from [ [-1, '-00:00'], - [-1, '-00'], [3630, '+01:00'], - [3630, '+01'], ]; } diff --git a/tests/ZonedDateTimeTest.php b/tests/ZonedDateTimeTest.php index ade120a..2382911 100644 --- a/tests/ZonedDateTimeTest.php +++ b/tests/ZonedDateTimeTest.php @@ -102,30 +102,6 @@ public static function providerOf(): array ['2011-06-30T12:34:56.123456789', '+17:00', '+17:00', 0, 1309376096, 123456789], ['2011-07-31T12:34:56.123456789', '+18:00', '+18:00', 0, 1312050896, 123456789], - ['2000-01-01T12:34:56.123456789', '-18', '-18:00', 0, 946794896, 123456789], - ['2001-02-02T12:34:56.123456789', '-17', '-17:00', 0, 981178496, 123456789], - ['2002-03-03T12:34:56.123456789', '-16', '-16:00', 0, 1015216496, 123456789], - ['2003-04-04T12:34:56.123456789', '-15', '-15:00', 0, 1049513696, 123456789], - ['2004-05-05T12:34:56.123456789', '-14', '-14:00', 0, 1083810896, 123456789], - ['2005-06-06T12:34:56.123456789', '-06', '-06:00', 0, 1118082896, 123456789], - ['2006-07-07T12:34:56.123456789', '-05', '-05:00', 0, 1152293696, 123456789], - ['2007-08-08T12:34:56.123456789', '-04', '-04:00', 0, 1186590896, 123456789], - ['2008-09-09T12:34:56.123456789', '-03', '-03:00', 0, 1220974496, 123456789], - ['2009-10-10T12:34:56.123456789', '-02', '-02:00', 0, 1255185296, 123456789], - ['2011-12-12T12:34:56.123456789', '-01', '-01:00', 0, 1323696896, 123456789], - ['2015-04-16T12:34:56.123456789', '+00', '+00:00', 0, 1429187696, 123456789], - ['2019-08-20T12:34:56.123456789', '+01', '+01:00', 0, 1566300896, 123456789], - ['2011-10-22T12:34:56.123456789', '+02', '+02:00', 0, 1319279696, 123456789], - ['2011-11-23T12:34:56.123456789', '+03', '+03:00', 0, 1322040896, 123456789], - ['2011-12-24T12:34:56.123456789', '+04', '+04:00', 0, 1324715696, 123456789], - ['2011-01-25T12:34:56.123456789', '+05', '+05:00', 0, 1295940896, 123456789], - ['2011-02-26T12:34:56.123456789', '+06', '+06:00', 0, 1298702096, 123456789], - ['2011-03-27T12:34:56.123456789', '+14', '+14:00', 0, 1301178896, 123456789], - ['2011-04-28T12:34:56.123456789', '+15', '+15:00', 0, 1303940096, 123456789], - ['2011-05-29T12:34:56.123456789', '+16', '+16:00', 0, 1306614896, 123456789], - ['2011-06-30T12:34:56.123456789', '+17', '+17:00', 0, 1309376096, 123456789], - ['2011-07-31T12:34:56.123456789', '+18', '+18:00', 0, 1312050896, 123456789], - // WITH REGION, NORMAL: NOT WITHIN A DST TRANSITION // The region is resolved to an offset without ambiguity. // The date-time is resolved to an instant without ambiguity. @@ -399,7 +375,6 @@ public static function providerParse(): iterable ['2001-02-03T01:02:03.456Z', '2001-02-03', '01:02:03.456', 'Z', 'Z'], ['2001-02-03T01:02-03:00', '2001-02-03', '01:02', '-03:00', '-03:00'], ['2001-02-03T01:02:03+04:00', '2001-02-03', '01:02:03', '+04:00', '+04:00'], - ['2001-02-03T01:02:03+04:00', '2001-02-03', '01:02:03', '+04', '+04:00'], ['2001-02-03T01:02:03.456+12:34', '2001-02-03', '01:02:03.456', '+12:34', '+12:34'], ['2001-02-03T01:02Z[Europe/London]', '2001-02-03', '01:02', 'Z', 'Europe/London'], ['2001-02-03T01:02+00:00[Europe/London]', '2001-02-03', '01:02', 'Z', 'Europe/London'], From 331f8094dedbcb1a2c9cb99226397c0a70078b0e Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Mon, 13 Oct 2025 10:25:38 +0300 Subject: [PATCH 3/6] Allow to parse TimeZoneOffset with hours only --- tests/TimeZoneOffsetTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/TimeZoneOffsetTest.php b/tests/TimeZoneOffsetTest.php index ad8fb38..b484dfc 100644 --- a/tests/TimeZoneOffsetTest.php +++ b/tests/TimeZoneOffsetTest.php @@ -152,6 +152,13 @@ public function testParse(string $text, int $totalSeconds): void public static function providerParse(): iterable { yield from [ + ['+00', 0], + ['-00', 0], + ['+01', 3600], + ['-01', -3600], + ['+18', 64800], + ['-18', -64800], + ['+00:00', 0], ['-00:00', 0], ['+01:00', 3600], @@ -182,7 +189,6 @@ public static function providerParseInvalidStringThrowsException(): array return [ [''], ['00:00'], - ['+00'], ['+00:'], ['+00:00:'], ['+1:00'], From dd68b2526f6543e68526509c21d5f91979822064 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Mon, 13 Oct 2025 10:25:46 +0300 Subject: [PATCH 4/6] Allow to parse TimeZoneOffset with hours only --- src/TimeZoneOffset.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TimeZoneOffset.php b/src/TimeZoneOffset.php index 60637b4..51933ec 100644 --- a/src/TimeZoneOffset.php +++ b/src/TimeZoneOffset.php @@ -128,6 +128,7 @@ public static function from(DateTimeParseResult $result): TimeZoneOffset * The following ISO 8601 formats are accepted: * * * `Z` - for UTC + * * `±hh` * * `±hh:mm` * * `±hh:mm:ss` * From e6b3478ddd3e2a21d9aa384296d9964daefa789d Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Wed, 15 Oct 2025 07:40:09 +0300 Subject: [PATCH 5/6] Allow to parse TimeZoneOffset with hours only --- src/Parser/IsoParsers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Parser/IsoParsers.php b/src/Parser/IsoParsers.php index 04922b2..a464911 100644 --- a/src/Parser/IsoParsers.php +++ b/src/Parser/IsoParsers.php @@ -206,7 +206,7 @@ public static function monthDay(): PatternParser } /** - * Returns a parser for a time-zone offset such as `Z` or `+01:00`. + * Returns a parser for a time-zone offset such as `Z`, `+01:00` or `+01`. */ public static function timeZoneOffset(): PatternParser { From eecc5d559d3bee5b7abc39bed8e9ccf2dc91e0c0 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Wed, 15 Oct 2025 08:46:02 +0300 Subject: [PATCH 6/6] Allow to parse TimeZoneOffset with hours only --- src/Parser/IsoParsers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Parser/IsoParsers.php b/src/Parser/IsoParsers.php index a464911..81f835a 100644 --- a/src/Parser/IsoParsers.php +++ b/src/Parser/IsoParsers.php @@ -206,7 +206,7 @@ public static function monthDay(): PatternParser } /** - * Returns a parser for a time-zone offset such as `Z`, `+01:00` or `+01`. + * Returns a parser for a time-zone offset such as `Z`, `+01`, `+01:00`, `+01:00:00`. */ public static function timeZoneOffset(): PatternParser {