Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.89% covered (success)
91.89%
34 / 37
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Codec
91.89% covered (success)
91.89%
34 / 37
77.78% covered (warning)
77.78%
7 / 9
29.45
0.00% covered (danger)
0.00%
0 / 1
 parse
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 normalize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 format
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 input
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromInput
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
6.10
 fromLegacy
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 timezone
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
5.02
 utc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clean
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\DateTime;
6
7use DateTimeImmutable;
8use DateTimeInterface;
9use DateTimeZone;
10use Throwable;
11
12final class Codec
13{
14    public const string STORAGE_FORMAT = 'Y-m-d\\TH:i:s\\Z';
15    public const string INPUT_FORMAT = 'Y-m-d\\TH:i:s';
16
17    private const string RFC3339 = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$/';
18
19    public static function parse(string $value): ?DateTimeImmutable
20    {
21        if (preg_match(self::RFC3339, $value) !== 1) {
22            return null;
23        }
24
25        $date = DateTimeImmutable::createFromFormat('!Y-m-d\\TH:i:sP', $value);
26
27        if ($date === false || !self::clean()) {
28            return null;
29        }
30
31        $expected = str_ends_with($value, 'Z') ? substr($value, 0, -1) . '+00:00' : $value;
32
33        return $date->format('Y-m-d\\TH:i:sP') === $expected ? $date : null;
34    }
35
36    public static function normalize(string $value): ?string
37    {
38        $date = self::parse($value);
39
40        return $date?->setTimezone(self::utc())->format(self::STORAGE_FORMAT);
41    }
42
43    public static function format(DateTimeInterface $value): string
44    {
45        return DateTimeImmutable::createFromInterface($value)
46            ->setTimezone(self::utc())
47            ->format(self::STORAGE_FORMAT);
48    }
49
50    public static function input(string $value, DateTimeZone $timezone): ?string
51    {
52        return self::parse($value)?->setTimezone($timezone)->format(self::INPUT_FORMAT);
53    }
54
55    public static function fromInput(string $value, DateTimeZone $timezone): ?string
56    {
57        if ($value === '') {
58            return '';
59        }
60
61        $formats = strlen($value) === 16 ? ['!Y-m-d\\TH:i', 'Y-m-d\\TH:i'] : ['!Y-m-d\\TH:i:s', 'Y-m-d\\TH:i:s'];
62        $date = DateTimeImmutable::createFromFormat($formats[0], $value, $timezone);
63
64        if ($date === false || !self::clean() || $date->format($formats[1]) !== $value) {
65            return null;
66        }
67
68        return self::format($date);
69    }
70
71    public static function fromLegacy(string $value, DateTimeZone $timezone): ?string
72    {
73        $normalized = self::normalize($value);
74
75        if ($normalized !== null) {
76            return $normalized;
77        }
78
79        if (str_contains($value, 'T')) {
80            return self::fromInput($value, $timezone);
81        }
82
83        $date = DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $value, $timezone);
84
85        if ($date === false || !self::clean() || $date->format('Y-m-d H:i:s') !== $value) {
86            return null;
87        }
88
89        return self::format($date);
90    }
91
92    public static function timezone(mixed $value): ?DateTimeZone
93    {
94        if (!is_string($value) || $value === '') {
95            return null;
96        }
97
98        try {
99            return new DateTimeZone($value);
100        } catch (Throwable) {
101            return null;
102        }
103    }
104
105    public static function utc(): DateTimeZone
106    {
107        return new DateTimeZone('UTC');
108    }
109
110    private static function clean(): bool
111    {
112        $errors = DateTimeImmutable::getLastErrors();
113
114        return $errors === false || $errors['warning_count'] === 0 && $errors['error_count'] === 0;
115    }
116}