diff --git a/README.md b/README.md index cdf896c..76ec29f 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,21 @@ $instant->toIso8601(); # 2026-02-17T16:30:00+00:00 $instant->toUnixSeconds(); # 1771345800 ``` +The offset accepts the ISO 8601 zulu designator (`Z`) as an alias for `+00:00`, with or without fractional seconds. + +```php +toIso8601(); # 2026-02-17T10:30:00+00:00 +$instant->toUnixSeconds(); # 1771324200 +``` + #### Creating from a database timestamp Parses a database date-time string as UTC, with or without microsecond precision (e.g. MySQL `DATETIME` diff --git a/src/Internal/Decoders/OffsetDateTimeDecoder.php b/src/Internal/Decoders/OffsetDateTimeDecoder.php index d148ad1..9554e0a 100644 --- a/src/Internal/Decoders/OffsetDateTimeDecoder.php +++ b/src/Internal/Decoders/OffsetDateTimeDecoder.php @@ -11,8 +11,8 @@ { private const string FORMAT = 'Y-m-d\TH:i:sP'; private const string FORMAT_MICRO = 'Y-m-d\TH:i:s.uP'; - private const string PATTERN = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+\-]\d{2}:\d{2}$/'; - private const string PATTERN_MICRO = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+\-]\d{2}:\d{2}$/'; + private const string PATTERN = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+\-]\d{2}:\d{2})$/'; + private const string PATTERN_MICRO = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+(?:Z|[+\-]\d{2}:\d{2})$/'; public function decode(string $value): ?DateTimeImmutable { diff --git a/tests/Unit/InstantTest.php b/tests/Unit/InstantTest.php index c469447..54fa394 100644 --- a/tests/Unit/InstantTest.php +++ b/tests/Unit/InstantTest.php @@ -1226,6 +1226,26 @@ public static function validStringsDataProvider(): array 'value' => '2026-02-17T16:00:00.500000+05:30', 'expectedIso8601' => '2026-02-17T10:30:00+00:00', 'expectedUnixSeconds' => 1771324200 + ], + 'Zulu designator' => [ + 'value' => '2026-02-17T10:30:00Z', + 'expectedIso8601' => '2026-02-17T10:30:00+00:00', + 'expectedUnixSeconds' => 1771324200 + ], + 'Zulu designator at midnight' => [ + 'value' => '2026-01-01T00:00:00Z', + 'expectedIso8601' => '2026-01-01T00:00:00+00:00', + 'expectedUnixSeconds' => 1767225600 + ], + 'Zulu with microseconds' => [ + 'value' => '2026-02-17T10:30:00.123456Z', + 'expectedIso8601' => '2026-02-17T10:30:00+00:00', + 'expectedUnixSeconds' => 1771324200 + ], + 'Zulu with short fraction' => [ + 'value' => '2026-02-17T10:30:00.272Z', + 'expectedIso8601' => '2026-02-17T10:30:00+00:00', + 'expectedUnixSeconds' => 1771324200 ] ]; } @@ -1243,7 +1263,7 @@ public static function invalidStringsDataProvider(): array 'Truncated offset' => ['value' => '2026-02-17T10:30:00+00'], 'Slash-separated date' => ['value' => '2026/02/17T10:30:00+00:00'], 'Missing time separator' => ['value' => '2026-02-17 10:30:00+00:00'], - 'Z suffix instead offset' => ['value' => '2026-02-17T10:30:00Z'], + 'Lowercase zulu designator' => ['value' => '2026-02-17T10:30:00z'], 'Unix timestamp as string' => ['value' => '1771324200'], 'Database format with invalid day' => ['value' => '2026-02-30 08:27:21.106011'], 'Database format with T separator' => ['value' => '2026-02-17T08:27:21.106011'],