1<?php
2
3
4namespace ComboStrap;
5
6
7use DateTime;
8use IntlDateFormatter;
9
10
11/**
12 * Class Is8601Date
13 * @package ComboStrap
14 * Format used by Google, Sqlite and others
15 *
16 * This is the date class of Combostrap
17 * that takes a valid input string
18 * and output an iso string
19 */
20class Iso8601Date
21{
22    public const CANONICAL = "date";
23    public const TIME_FORMATTER_TYPE = IntlDateFormatter::NONE;
24    public const DATE_FORMATTER_TYPE = IntlDateFormatter::TRADITIONAL;
25    /**
26     * @var DateTime|false
27     */
28    private $dateTime;
29
30    /**
31     * ATOM = IS08601
32     * See {@link Iso8601Date::getFormat()} for more information
33     */
34    private const VALID_FORMATS = [
35        \DateTimeInterface::ATOM,
36        'Y-m-d H:i:sP',
37        'Y-m-d H:i:s',
38        'Y-m-d H:i',
39        'Y-m-d H',
40        'Y-m-d',
41    ];
42
43
44    /**
45     * Date constructor.
46     */
47    public function __construct($dateTime = null)
48    {
49
50        if ($dateTime == null) {
51
52            $this->dateTime = new DateTime();
53
54        } else {
55
56            $this->dateTime = $dateTime;
57
58        }
59
60    }
61
62    /**
63     * @param $dateString
64     * @return Iso8601Date
65     * @throws ExceptionBadSyntax if the format is not supported
66     */
67    public static function createFromString(string $dateString): Iso8601Date
68    {
69
70        $original = $dateString;
71
72        $dateString = trim($dateString);
73
74        /**
75         * Time ?
76         * (ie only YYYY-MM-DD)
77         */
78        if (strlen($dateString) <= 10) {
79            /**
80             * We had the time to 00:00:00
81             * because {@link DateTime::createFromFormat} with a format of
82             * Y-m-d will be using the actual time otherwise
83             *
84             */
85            $dateString .= "T00:00:00";
86        }
87
88        /**
89         * Space as T
90         */
91        $dateString = str_replace(" ", "T", $dateString);
92
93
94        if (strlen($dateString) <= 13) {
95            /**
96             * We had the time to 00:00:00
97             * because {@link DateTime::createFromFormat} with a format of
98             * Y-m-d will be using the actual time otherwise
99             *
100             */
101            $dateString .= ":00:00";
102        }
103
104        if (strlen($dateString) <= 16) {
105            /**
106             * We had the time to 00:00:00
107             * because {@link DateTime::createFromFormat} with a format of
108             * Y-m-d will be using the actual time otherwise
109             *
110             */
111            $dateString .= ":00";
112        }
113
114        /**
115         * Timezone
116         */
117        if (strlen($dateString) <= 19) {
118            /**
119             * Because this text metadata may be used in other part of the application
120             * We add the timezone to make it whole
121             * And to have a consistent value
122             */
123            $dateString .= date('P');
124        }
125
126
127        $dateTime = DateTime::createFromFormat(self::getFormat(), $dateString);
128        if ($dateTime === false) {
129            $message = "The date string ($original) is not in a valid date format. (" . join(", ", self::VALID_FORMATS) . ")";
130            throw new ExceptionBadSyntax($message, self::CANONICAL);
131        }
132        return new Iso8601Date($dateTime);
133
134    }
135
136    public static function createFromTimestamp($timestamp): Iso8601Date
137    {
138        $dateTime = new DateTime();
139        $dateTime->setTimestamp($timestamp);
140        return new Iso8601Date($dateTime);
141    }
142
143    /**
144     * And note {@link DATE_ISO8601}
145     * because it's not the compliant IS0-8601 format
146     * as explained here
147     * https://www.php.net/manual/en/class.datetimeinterface.php#datetime.constants.iso8601
148     * ATOM is
149     *
150     * This format is used by Sqlite, Google and is pretty the standard everywhere
151     * https://www.w3.org/TR/NOTE-datetime
152     */
153    public static function getFormat(): string
154    {
155        return DATE_ATOM;
156    }
157
158    /**
159     *
160     */
161    public static function isValid($value): bool
162    {
163        try {
164            $dateObject = Iso8601Date::createFromString($value);
165            return $dateObject->isValidDateEntry(); // ??? Validation seems to be at construction
166        } catch (ExceptionBadSyntax $e) {
167            return false;
168        }
169    }
170
171    public function isValidDateEntry(): bool
172    {
173        if ($this->dateTime !== false) {
174            return true;
175        } else {
176            return false;
177        }
178    }
179
180    public static function createFromDateTime(DateTime $dateTime): Iso8601Date
181    {
182        return new Iso8601Date($dateTime);
183    }
184
185    public static function createFromNow(): Iso8601Date
186    {
187        return new Iso8601Date();
188    }
189
190    /**
191     * @throws ExceptionNotFound
192     */
193    public static function getInternationalFormatter($constant): int
194    {
195        $constantNormalized = trim(strtolower($constant));
196        switch ($constantNormalized) {
197            case "none":
198                return IntlDateFormatter::NONE;
199            case "full":
200                return IntlDateFormatter::FULL;
201            case "relativefull":
202                return IntlDateFormatter::RELATIVE_FULL;
203            case "long":
204                return IntlDateFormatter::LONG;
205            case "relativelong":
206                return IntlDateFormatter::RELATIVE_LONG;
207            case "medium":
208                return IntlDateFormatter::MEDIUM;
209            case "relativemedium":
210                return IntlDateFormatter::RELATIVE_MEDIUM;
211            case "short":
212                return IntlDateFormatter::SHORT;
213            case "relativeshort":
214                return IntlDateFormatter::RELATIVE_SHORT;
215            case "traditional":
216                return IntlDateFormatter::TRADITIONAL;
217            default:
218                throw new ExceptionNotFound("The constant ($constant) is not a valid constant", self::CANONICAL);
219        }
220    }
221
222    public function getDateTime()
223    {
224        return $this->dateTime;
225    }
226
227    public function __toString()
228    {
229        return $this->getDateTime()->format(self::getFormat());
230    }
231
232    public function toIsoStringMs()
233    {
234        return $this->getDateTime()->format("Y-m-d\TH:i:s.u");
235    }
236
237    /**
238     * Shortcut to {@link DateTime::format()}
239     * Format only in English
240     * @param $string
241     * @return string
242     * @link https://php.net/manual/en/datetime.format.php
243     */
244    public function format($string): string
245    {
246        return $this->getDateTime()->format($string);
247    }
248
249    public function toString()
250    {
251        return $this->__toString();
252    }
253
254    /**
255     * @throws ExceptionBadSyntax
256     */
257    public function formatLocale($pattern = null, $locale = null)
258    {
259
260        /**
261         * https://www.php.net/manual/en/function.strftime.php
262         * As been deprecated
263         * The only alternative with local is
264         * https://www.php.net/manual/en/intldateformatter.format.php
265         *
266         * Based on ISO date
267         * ICU Date formatter: https://unicode-org.github.io/icu-docs/#/icu4c/udat_8h.html
268         * ICU Date formats: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
269         * ICU User Guide: https://unicode-org.github.io/icu/userguide/
270         * ICU Formatting Dates and Times: https://unicode-org.github.io/icu/userguide/format_parse/datetime/
271         */
272        if (strpos($pattern, "%") !== false) {
273            LogUtility::warning("The date format ($pattern) is no more supported. Why ? Because Php has deprecated <a href=\"https://www.php.net/manual/en/function.strftime.php\">strftime</a>. You need to use the <a href=\"https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax\">Unicode Date Time format</a>", self::CANONICAL);
274            return strftime($pattern, $this->dateTime->getTimestamp());
275        }
276
277        /**
278         * This parameters
279         * are used to format date with the locale
280         * when the pattern is null
281         * Doc: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#producing-normal-date-formats-for-a-locale
282         *
283         * They may be null by the way.
284         *
285         */
286        $dateType = self::DATE_FORMATTER_TYPE;
287        $timeType = self::TIME_FORMATTER_TYPE;
288        if ($pattern !== null) {
289            $normalFormat = explode(" ", $pattern);
290            if (sizeof($normalFormat) === 2) {
291                try {
292                    $dateType = self::getInternationalFormatter($normalFormat[0]);
293                    $timeType = self::getInternationalFormatter($normalFormat[1]);
294                    $pattern = null;
295                } catch (ExceptionNotFound $e) {
296                    // ok
297                }
298            }
299        }
300
301        /**
302         * Formatter instantiation
303         */
304        $formatter = datefmt_create(
305            $locale,
306            $dateType,
307            $timeType,
308            $this->dateTime->getTimezone(),
309            IntlDateFormatter::GREGORIAN,
310            $pattern
311        );
312        $formatted = datefmt_format($formatter, $this->dateTime);
313        if ($formatted === false) {
314            if ($locale === null) {
315                $locale = "";
316            }
317            if ($pattern === null) {
318                $pattern = "";
319            }
320            throw new ExceptionBadSyntax("Unable to format the date with the pattern ($pattern) and locale ($locale)");
321        }
322        return $formatted;
323    }
324
325    public function olderThan(DateTime $rightTime): bool
326    {
327
328        $internalMs = DataType::toMilliSeconds($this->dateTime);
329        $externalMilliSeconds = DataType::toMilliSeconds($rightTime);
330        if ($externalMilliSeconds > $internalMs) {
331            return true;
332        }
333        return false;
334
335    }
336
337    public function diff(DateTime $rightTime): \DateInterval
338    {
339        // example get the s part of the diff (even if there is day of diff)
340        // $seconds = $diff->format('%s');
341        return $this->dateTime->diff($rightTime, true);
342    }
343
344
345}
346