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         * https://www.php.net/manual/en/function.strftime.php
261         * As been deprecated
262         * The only alternative with local is
263         * https://www.php.net/manual/en/intldateformatter.format.php
264         *
265         * Based on ISO date
266         * ICU Date formatter: https://unicode-org.github.io/icu-docs/#/icu4c/udat_8h.html
267         * ICU Date formats: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
268         * ICU User Guide: https://unicode-org.github.io/icu/userguide/
269         * ICU Formatting Dates and Times: https://unicode-org.github.io/icu/userguide/format_parse/datetime/
270         */
271        if (strpos($pattern, "%") !== false) {
272            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);
273            return strftime($pattern, $this->dateTime->getTimestamp());
274        }
275
276        /**
277         * This parameters
278         * are used to format date with the locale
279         * when the pattern is null
280         * Doc: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#producing-normal-date-formats-for-a-locale
281         *
282         * They may be null by the way.
283         *
284         */
285        $dateType = self::DATE_FORMATTER_TYPE;
286        $timeType = self::TIME_FORMATTER_TYPE;
287        if ($pattern !== null) {
288            $normalFormat = explode(" ", $pattern);
289            if (sizeof($normalFormat) === 2) {
290                try {
291                    $dateType = self::getInternationalFormatter($normalFormat[0]);
292                    $timeType = self::getInternationalFormatter($normalFormat[1]);
293                    $pattern = null;
294                } catch (ExceptionNotFound $e) {
295                    // ok
296                }
297            }
298        }
299
300        /**
301         * Formatter instantiation
302         * https://www.php.net/manual/en/intldateformatter.create.php
303         * List of local: with ResourceBundle::getLocales('')
304         */
305        $intlDateFormatter = datefmt_create(
306            $locale,
307            $dateType,
308            $timeType,
309            $this->dateTime->getTimezone(),
310            IntlDateFormatter::GREGORIAN,
311            $pattern
312        );
313        try {
314            $formatted = datefmt_format($intlDateFormatter, $this->dateTime);
315        } catch (\Error $e) {
316            // Found unconstructed IntlDateFormatter
317            // No idea how I can check that before formatting
318            LogUtility::warning("Locale value ($locale) is an unknown ICU locale. Using default locale instead", self::CANONICAL);
319            $intlDateFormatter = datefmt_create(
320                null,
321                $dateType,
322                $timeType,
323                $this->dateTime->getTimezone(),
324                IntlDateFormatter::GREGORIAN,
325                $pattern
326            );
327            $formatted = datefmt_format($intlDateFormatter, $this->dateTime);
328        }
329        if ($formatted === false) {
330            if ($locale === null) {
331                $locale = "";
332            }
333            if ($pattern === null) {
334                $pattern = "";
335            }
336            throw new ExceptionBadSyntax("Unable to format the date with the pattern ($pattern) and locale ($locale)");
337        }
338        return $formatted;
339    }
340
341    public function olderThan(DateTime $rightTime): bool
342    {
343
344        $internalMs = DataType::toMilliSeconds($this->dateTime);
345        $externalMilliSeconds = DataType::toMilliSeconds($rightTime);
346        if ($externalMilliSeconds > $internalMs) {
347            return true;
348        }
349        return false;
350
351    }
352
353    public function diff(DateTime $rightTime): \DateInterval
354    {
355        // example get the s part of the diff (even if there is day of diff)
356        // $seconds = $diff->format('%s');
357        return $this->dateTime->diff($rightTime, true);
358    }
359
360
361}
362