Skip to content

Commit

Permalink
Better support RFC3339 (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
GrahamCampbell authored Apr 24, 2023
1 parent 298474a commit f819b5f
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 35 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
php-version: '7.4'
tools: composer:v2
coverage: none
env:
Expand All @@ -44,7 +44,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
php-version: '7.4'
tools: composer:v2
coverage: none
env:
Expand Down
46 changes: 18 additions & 28 deletions src/Utilities/TimeFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ final class TimeFormatter
private const TIME_FORMAT = 'Y-m-d\TH:i:s\Z';
private const TIME_ZONE = 'UTC';

private const RFC3339_FORMAT = 'Y-m-d\TH:i:sP';
private const RFC3339_EXTENDED_FORMAT = 'Y-m-d\TH:i:s.uP';

public static function encode(?DateTimeImmutable $time): ?string
{
if ($time === null) {
Expand All @@ -31,45 +34,32 @@ public static function decode(?string $time): ?DateTimeImmutable
return null;
}

$time = self::trimMicroseconds($time);
$time = \strtoupper($time);

/** @psalm-suppress UndefinedFunction */
$decoded = \str_contains($time, '.')
? DateTimeImmutable::createFromFormat(self::RFC3339_EXTENDED_FORMAT, self::truncateOverPrecision($time), new DateTimeZone(self::TIME_ZONE))
: DateTimeImmutable::createFromFormat(self::RFC3339_FORMAT, $time, new DateTimeZone(self::TIME_ZONE));

try {
$decoded = new DateTimeImmutable($time);
} catch (\Throwable $th) {
if ($decoded === false) {
throw new ValueError(
\sprintf('%s(): Argument #1 ($time) is not a valid RFC3339 timestamp', __METHOD__)
);
}

return self::shiftWithTimezone($time, $decoded);
return $decoded;
}

private static function trimMicroseconds(string $time): string
private static function truncateOverPrecision(string $time): string
{
$microseconds = explode('.', $time, 2);
if (isset($microseconds[1])) {
$microsecondsAndTimezone = explode('+', $microseconds[1], 2);
if (count($microsecondsAndTimezone) === 1) {
$microsecondsAndTimezone = explode('-', $microseconds[1], 2);
}
$timezone = isset($microsecondsAndTimezone[1]) ? sprintf('+%s', $microsecondsAndTimezone[1]) : '';
$time = sprintf(
"%s.%s%s",
$microseconds[0],
substr($microsecondsAndTimezone[0], 0, 6),
$timezone
);
}
[$fst, $snd] = explode('.', $time);

return $time;
}
// match the first n digits at the start
\preg_match('/^\d+/', $snd, $matches);

private static function shiftWithTimezone(string $time, DateTimeImmutable $datetime): DateTimeImmutable
{
if (\strpos($time, '+') === false && \strpos($time, '-') === false && \strtoupper(\substr($time, -1)) !== 'Z') {
return $datetime->setTimezone(new \DateTimeZone('UTC'));
}
$digits = $matches[0] ?? '';

return $datetime;
// datetime portion + period + up to 6 digits + timezone string
return $fst . '.' . substr($digits, 0, 6) . substr($snd, strlen($digits));
}
}
33 changes: 28 additions & 5 deletions tests/Unit/Utilities/TimeFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ public function testEncode(): void
);
}

public static function providesDecodeCases(): array
public static function providesValidDecodeCases(): array
{
return [
// UTC
['2018-04-05T17:31:00Z', '2018-04-05t17:31:00Z'],
['2018-04-05T17:31:00Z', '2018-04-05T17:31:00Z'],
['1985-04-12T23:20:50.100000Z', '1985-04-12T23:20:50.1Z'],
['1985-04-12T23:20:50.100000Z', '1985-04-12T23:20:50.10Z'],
Expand All @@ -41,6 +42,7 @@ public static function providesDecodeCases(): array
['1985-04-12T23:20:50.123456Z', '1985-04-12T23:20:50.123456789Z'],

// +01:00
['2018-04-05T16:31:00Z', '2018-04-05t17:31:00+01:00'],
['2018-04-05T16:31:00Z', '2018-04-05T17:31:00+01:00'],
['1985-04-12T22:20:50.100000Z', '1985-04-12T23:20:50.1+01:00'],
['1985-04-12T22:20:50.100000Z', '1985-04-12T23:20:50.10+01:00'],
Expand All @@ -57,12 +59,17 @@ public static function providesDecodeCases(): array
['1985-04-12T22:20:50.123456Z', '1985-04-12T23:20:50.1234567+01:00'],
['1985-04-12T22:20:50.123456Z', '1985-04-12T23:20:50.12345678+01:00'],
['1985-04-12T22:20:50.123456Z', '1985-04-12T23:20:50.123456789+01:00'],
['1985-04-12T22:20:50.123456Z', '1985-04-12T23:20:50.1234567890+01:00'],

// -05:00
['2018-04-05T22:31:00Z', '2018-04-05t17:31:00-05:00'],
['2018-04-05T22:31:00Z', '2018-04-05T17:31:00-05:00'],
['1985-04-13T04:20:50.123456Z', '1985-04-12T23:20:50.123456-05:00'],
['1985-04-13T04:20:50.123456Z', '1985-04-12T23:20:50.123456789-05:00'],
];
}

/**
* @dataProvider providesDecodeCases
* @dataProvider providesValidDecodeCases
*/
public function testDecode(string $expected, string $input): void
{
Expand All @@ -88,14 +95,30 @@ public function testDecodeEmpty(): void
);
}

public function testDecodeInvalidTime(): void
public static function providesInvalidDecodeCases(): array
{
return [
[''],
['123'],
['2018asdsdsafd'],
['2018-04-05'],
['2018-04-05 17:31:00Z'],
['2018-04-05T17:31:00.Z'],
['2018-04-05T17:31:00ZZ'],
];
}

/**
* @dataProvider providesInvalidDecodeCases
*/
public function testDecodeInvalidTime(string $input): void
{
$this->expectException(ValueError::class);

$this->expectExceptionMessage(
'CloudEvents\\Utilities\\TimeFormatter::decode(): Argument #1 ($time) is not a valid RFC3339 timestamp'
);

TimeFormatter::decode('2018asdsdsafd');
TimeFormatter::decode($input);
}
}

0 comments on commit f819b5f

Please sign in to comment.