xref: /template/strap/ComboStrap/Iso8601Date.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
137748cd8SNickeau<?php
237748cd8SNickeau
337748cd8SNickeau
437748cd8SNickeaunamespace ComboStrap;
537748cd8SNickeau
637748cd8SNickeau
737748cd8SNickeauuse DateTime;
8*04fd306cSNickeauuse IntlDateFormatter;
937748cd8SNickeau
1037748cd8SNickeau
1137748cd8SNickeau/**
1237748cd8SNickeau * Class Is8601Date
1337748cd8SNickeau * @package ComboStrap
1437748cd8SNickeau * Format used by Google, Sqlite and others
151fa8c418SNickeau *
161fa8c418SNickeau * This is the date class of Combostrap
171fa8c418SNickeau * that takes a valid input string
181fa8c418SNickeau * and output an iso string
1937748cd8SNickeau */
2037748cd8SNickeauclass Iso8601Date
2137748cd8SNickeau{
221fa8c418SNickeau    public const CANONICAL = "date";
23*04fd306cSNickeau    public const TIME_FORMATTER_TYPE = IntlDateFormatter::NONE;
24*04fd306cSNickeau    public const DATE_FORMATTER_TYPE = IntlDateFormatter::TRADITIONAL;
2537748cd8SNickeau    /**
2637748cd8SNickeau     * @var DateTime|false
2737748cd8SNickeau     */
2837748cd8SNickeau    private $dateTime;
2937748cd8SNickeau
301fa8c418SNickeau    /**
311fa8c418SNickeau     * ATOM = IS08601
321fa8c418SNickeau     * See {@link Iso8601Date::getFormat()} for more information
331fa8c418SNickeau     */
341fa8c418SNickeau    private const VALID_FORMATS = [
351fa8c418SNickeau        \DateTimeInterface::ATOM,
361fa8c418SNickeau        'Y-m-d H:i:sP',
371fa8c418SNickeau        'Y-m-d H:i:s',
381fa8c418SNickeau        'Y-m-d H:i',
391fa8c418SNickeau        'Y-m-d H',
401fa8c418SNickeau        'Y-m-d',
411fa8c418SNickeau    ];
421fa8c418SNickeau
4337748cd8SNickeau
4437748cd8SNickeau    /**
4537748cd8SNickeau     * Date constructor.
4637748cd8SNickeau     */
4737748cd8SNickeau    public function __construct($dateTime = null)
4837748cd8SNickeau    {
4937748cd8SNickeau
5037748cd8SNickeau        if ($dateTime == null) {
5137748cd8SNickeau
5237748cd8SNickeau            $this->dateTime = new DateTime();
5337748cd8SNickeau
5437748cd8SNickeau        } else {
5537748cd8SNickeau
5637748cd8SNickeau            $this->dateTime = $dateTime;
5737748cd8SNickeau
5837748cd8SNickeau        }
5937748cd8SNickeau
6037748cd8SNickeau    }
6137748cd8SNickeau
62c3437056SNickeau    /**
63*04fd306cSNickeau     * @param $dateString
64c3437056SNickeau     * @return Iso8601Date
65*04fd306cSNickeau     * @throws ExceptionBadSyntax if the format is not supported
66c3437056SNickeau     */
67*04fd306cSNickeau    public static function createFromString(string $dateString): Iso8601Date
6837748cd8SNickeau    {
691fa8c418SNickeau
701fa8c418SNickeau        $original = $dateString;
711fa8c418SNickeau
72*04fd306cSNickeau        $dateString = trim($dateString);
7337748cd8SNickeau
7437748cd8SNickeau        /**
7537748cd8SNickeau         * Time ?
7637748cd8SNickeau         * (ie only YYYY-MM-DD)
7737748cd8SNickeau         */
781fa8c418SNickeau        if (strlen($dateString) <= 10) {
7937748cd8SNickeau            /**
8037748cd8SNickeau             * We had the time to 00:00:00
8137748cd8SNickeau             * because {@link DateTime::createFromFormat} with a format of
8237748cd8SNickeau             * Y-m-d will be using the actual time otherwise
8337748cd8SNickeau             *
8437748cd8SNickeau             */
851fa8c418SNickeau            $dateString .= "T00:00:00";
861fa8c418SNickeau        }
871fa8c418SNickeau
881fa8c418SNickeau        /**
891fa8c418SNickeau         * Space as T
901fa8c418SNickeau         */
911fa8c418SNickeau        $dateString = str_replace(" ", "T", $dateString);
921fa8c418SNickeau
931fa8c418SNickeau
941fa8c418SNickeau        if (strlen($dateString) <= 13) {
951fa8c418SNickeau            /**
961fa8c418SNickeau             * We had the time to 00:00:00
971fa8c418SNickeau             * because {@link DateTime::createFromFormat} with a format of
981fa8c418SNickeau             * Y-m-d will be using the actual time otherwise
991fa8c418SNickeau             *
1001fa8c418SNickeau             */
1011fa8c418SNickeau            $dateString .= ":00:00";
1021fa8c418SNickeau        }
1031fa8c418SNickeau
1041fa8c418SNickeau        if (strlen($dateString) <= 16) {
1051fa8c418SNickeau            /**
1061fa8c418SNickeau             * We had the time to 00:00:00
1071fa8c418SNickeau             * because {@link DateTime::createFromFormat} with a format of
1081fa8c418SNickeau             * Y-m-d will be using the actual time otherwise
1091fa8c418SNickeau             *
1101fa8c418SNickeau             */
1111fa8c418SNickeau            $dateString .= ":00";
11237748cd8SNickeau        }
11337748cd8SNickeau
11437748cd8SNickeau        /**
11537748cd8SNickeau         * Timezone
11637748cd8SNickeau         */
1171fa8c418SNickeau        if (strlen($dateString) <= 19) {
11837748cd8SNickeau            /**
11937748cd8SNickeau             * Because this text metadata may be used in other part of the application
12037748cd8SNickeau             * We add the timezone to make it whole
12137748cd8SNickeau             * And to have a consistent value
12237748cd8SNickeau             */
1231fa8c418SNickeau            $dateString .= date('P');
12437748cd8SNickeau        }
12537748cd8SNickeau
1261fa8c418SNickeau
12726a7e0f8Sgerardnico        $dateTime = DateTime::createFromFormat(self::getFormat(), $dateString);
1281fa8c418SNickeau        if ($dateTime === false) {
129c3437056SNickeau            $message = "The date string ($original) is not in a valid date format. (" . join(", ", self::VALID_FORMATS) . ")";
130*04fd306cSNickeau            throw new ExceptionBadSyntax($message, self::CANONICAL);
1311fa8c418SNickeau        }
13237748cd8SNickeau        return new Iso8601Date($dateTime);
1331fa8c418SNickeau
13437748cd8SNickeau    }
13537748cd8SNickeau
1361fa8c418SNickeau    public static function createFromTimestamp($timestamp): Iso8601Date
13737748cd8SNickeau    {
13837748cd8SNickeau        $dateTime = new DateTime();
13937748cd8SNickeau        $dateTime->setTimestamp($timestamp);
14037748cd8SNickeau        return new Iso8601Date($dateTime);
14137748cd8SNickeau    }
14237748cd8SNickeau
14337748cd8SNickeau    /**
14437748cd8SNickeau     * And note {@link DATE_ISO8601}
14537748cd8SNickeau     * because it's not the compliant IS0-8601 format
14637748cd8SNickeau     * as explained here
14737748cd8SNickeau     * https://www.php.net/manual/en/class.datetimeinterface.php#datetime.constants.iso8601
14837748cd8SNickeau     * ATOM is
14937748cd8SNickeau     *
15037748cd8SNickeau     * This format is used by Sqlite, Google and is pretty the standard everywhere
15137748cd8SNickeau     * https://www.w3.org/TR/NOTE-datetime
15237748cd8SNickeau     */
1531fa8c418SNickeau    public static function getFormat(): string
15437748cd8SNickeau    {
15537748cd8SNickeau        return DATE_ATOM;
15637748cd8SNickeau    }
15737748cd8SNickeau
158*04fd306cSNickeau    /**
159*04fd306cSNickeau     *
160*04fd306cSNickeau     */
161c3437056SNickeau    public static function isValid($value): bool
162c3437056SNickeau    {
163*04fd306cSNickeau        try {
164c3437056SNickeau            $dateObject = Iso8601Date::createFromString($value);
165*04fd306cSNickeau            return $dateObject->isValidDateEntry(); // ??? Validation seems to be at construction
166*04fd306cSNickeau        } catch (ExceptionBadSyntax $e) {
167*04fd306cSNickeau            return false;
168*04fd306cSNickeau        }
169c3437056SNickeau    }
170c3437056SNickeau
171c3437056SNickeau    public function isValidDateEntry(): bool
17237748cd8SNickeau    {
17337748cd8SNickeau        if ($this->dateTime !== false) {
17437748cd8SNickeau            return true;
17537748cd8SNickeau        } else {
17637748cd8SNickeau            return false;
17737748cd8SNickeau        }
17837748cd8SNickeau    }
17937748cd8SNickeau
180c3437056SNickeau    public static function createFromDateTime(DateTime $dateTime): Iso8601Date
181c3437056SNickeau    {
182c3437056SNickeau        return new Iso8601Date($dateTime);
183c3437056SNickeau    }
184c3437056SNickeau
185c3437056SNickeau    public static function createFromNow(): Iso8601Date
186c3437056SNickeau    {
187c3437056SNickeau        return new Iso8601Date();
188c3437056SNickeau    }
189c3437056SNickeau
190*04fd306cSNickeau    /**
191*04fd306cSNickeau     * @throws ExceptionNotFound
192*04fd306cSNickeau     */
193*04fd306cSNickeau    public static function getInternationalFormatter($constant): int
194*04fd306cSNickeau    {
195*04fd306cSNickeau        $constantNormalized = trim(strtolower($constant));
196*04fd306cSNickeau        switch ($constantNormalized) {
197*04fd306cSNickeau            case "none":
198*04fd306cSNickeau                return IntlDateFormatter::NONE;
199*04fd306cSNickeau            case "full":
200*04fd306cSNickeau                return IntlDateFormatter::FULL;
201*04fd306cSNickeau            case "relativefull":
202*04fd306cSNickeau                return IntlDateFormatter::RELATIVE_FULL;
203*04fd306cSNickeau            case "long":
204*04fd306cSNickeau                return IntlDateFormatter::LONG;
205*04fd306cSNickeau            case "relativelong":
206*04fd306cSNickeau                return IntlDateFormatter::RELATIVE_LONG;
207*04fd306cSNickeau            case "medium":
208*04fd306cSNickeau                return IntlDateFormatter::MEDIUM;
209*04fd306cSNickeau            case "relativemedium":
210*04fd306cSNickeau                return IntlDateFormatter::RELATIVE_MEDIUM;
211*04fd306cSNickeau            case "short":
212*04fd306cSNickeau                return IntlDateFormatter::SHORT;
213*04fd306cSNickeau            case "relativeshort":
214*04fd306cSNickeau                return IntlDateFormatter::RELATIVE_SHORT;
215*04fd306cSNickeau            case "traditional":
216*04fd306cSNickeau                return IntlDateFormatter::TRADITIONAL;
217*04fd306cSNickeau            default:
218*04fd306cSNickeau                throw new ExceptionNotFound("The constant ($constant) is not a valid constant", self::CANONICAL);
219*04fd306cSNickeau        }
220*04fd306cSNickeau    }
221*04fd306cSNickeau
22237748cd8SNickeau    public function getDateTime()
22337748cd8SNickeau    {
22437748cd8SNickeau        return $this->dateTime;
22537748cd8SNickeau    }
22637748cd8SNickeau
22737748cd8SNickeau    public function __toString()
22837748cd8SNickeau    {
22937748cd8SNickeau        return $this->getDateTime()->format(self::getFormat());
23037748cd8SNickeau    }
23137748cd8SNickeau
232*04fd306cSNickeau    public function toIsoStringMs()
233*04fd306cSNickeau    {
234*04fd306cSNickeau        return $this->getDateTime()->format("Y-m-d\TH:i:s.u");
235*04fd306cSNickeau    }
236*04fd306cSNickeau
23737748cd8SNickeau    /**
23837748cd8SNickeau     * Shortcut to {@link DateTime::format()}
239*04fd306cSNickeau     * Format only in English
24037748cd8SNickeau     * @param $string
24137748cd8SNickeau     * @return string
24237748cd8SNickeau     * @link https://php.net/manual/en/datetime.format.php
24337748cd8SNickeau     */
2441fa8c418SNickeau    public function format($string): string
24537748cd8SNickeau    {
24637748cd8SNickeau        return $this->getDateTime()->format($string);
24737748cd8SNickeau    }
24837748cd8SNickeau
249c3437056SNickeau    public function toString()
250c3437056SNickeau    {
251c3437056SNickeau        return $this->__toString();
252c3437056SNickeau    }
253c3437056SNickeau
254*04fd306cSNickeau    /**
255*04fd306cSNickeau     * @throws ExceptionBadSyntax
256*04fd306cSNickeau     */
257*04fd306cSNickeau    public function formatLocale($pattern = null, $locale = null)
258*04fd306cSNickeau    {
259c3437056SNickeau
260*04fd306cSNickeau        /**
261*04fd306cSNickeau         * https://www.php.net/manual/en/function.strftime.php
262*04fd306cSNickeau         * As been deprecated
263*04fd306cSNickeau         * The only alternative with local is
264*04fd306cSNickeau         * https://www.php.net/manual/en/intldateformatter.format.php
265*04fd306cSNickeau         *
266*04fd306cSNickeau         * Based on ISO date
267*04fd306cSNickeau         * ICU Date formatter: https://unicode-org.github.io/icu-docs/#/icu4c/udat_8h.html
268*04fd306cSNickeau         * ICU Date formats: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
269*04fd306cSNickeau         * ICU User Guide: https://unicode-org.github.io/icu/userguide/
270*04fd306cSNickeau         * ICU Formatting Dates and Times: https://unicode-org.github.io/icu/userguide/format_parse/datetime/
271*04fd306cSNickeau         */
272*04fd306cSNickeau        if (strpos($pattern, "%") !== false) {
273*04fd306cSNickeau            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*04fd306cSNickeau            return strftime($pattern, $this->dateTime->getTimestamp());
275*04fd306cSNickeau        }
276*04fd306cSNickeau
277*04fd306cSNickeau        /**
278*04fd306cSNickeau         * This parameters
279*04fd306cSNickeau         * are used to format date with the locale
280*04fd306cSNickeau         * when the pattern is null
281*04fd306cSNickeau         * Doc: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#producing-normal-date-formats-for-a-locale
282*04fd306cSNickeau         *
283*04fd306cSNickeau         * They may be null by the way.
284*04fd306cSNickeau         *
285*04fd306cSNickeau         */
286*04fd306cSNickeau        $dateType = self::DATE_FORMATTER_TYPE;
287*04fd306cSNickeau        $timeType = self::TIME_FORMATTER_TYPE;
288*04fd306cSNickeau        if ($pattern !== null) {
289*04fd306cSNickeau            $normalFormat = explode(" ", $pattern);
290*04fd306cSNickeau            if (sizeof($normalFormat) === 2) {
291*04fd306cSNickeau                try {
292*04fd306cSNickeau                    $dateType = self::getInternationalFormatter($normalFormat[0]);
293*04fd306cSNickeau                    $timeType = self::getInternationalFormatter($normalFormat[1]);
294*04fd306cSNickeau                    $pattern = null;
295*04fd306cSNickeau                } catch (ExceptionNotFound $e) {
296*04fd306cSNickeau                    // ok
297*04fd306cSNickeau                }
298*04fd306cSNickeau            }
299*04fd306cSNickeau        }
300*04fd306cSNickeau
301*04fd306cSNickeau        /**
302*04fd306cSNickeau         * Formatter instantiation
303*04fd306cSNickeau         */
304*04fd306cSNickeau        $formatter = datefmt_create(
305*04fd306cSNickeau            $locale,
306*04fd306cSNickeau            $dateType,
307*04fd306cSNickeau            $timeType,
308*04fd306cSNickeau            $this->dateTime->getTimezone(),
309*04fd306cSNickeau            IntlDateFormatter::GREGORIAN,
310*04fd306cSNickeau            $pattern
311*04fd306cSNickeau        );
312*04fd306cSNickeau        $formatted = datefmt_format($formatter, $this->dateTime);
313*04fd306cSNickeau        if ($formatted === false) {
314*04fd306cSNickeau            if ($locale === null) {
315*04fd306cSNickeau                $locale = "";
316*04fd306cSNickeau            }
317*04fd306cSNickeau            if ($pattern === null) {
318*04fd306cSNickeau                $pattern = "";
319*04fd306cSNickeau            }
320*04fd306cSNickeau            throw new ExceptionBadSyntax("Unable to format the date with the pattern ($pattern) and locale ($locale)");
321*04fd306cSNickeau        }
322*04fd306cSNickeau        return $formatted;
323*04fd306cSNickeau    }
324*04fd306cSNickeau
325*04fd306cSNickeau    public function olderThan(DateTime $rightTime): bool
326*04fd306cSNickeau    {
327*04fd306cSNickeau
328*04fd306cSNickeau        $internalMs = DataType::toMilliSeconds($this->dateTime);
329*04fd306cSNickeau        $externalMilliSeconds = DataType::toMilliSeconds($rightTime);
330*04fd306cSNickeau        if ($externalMilliSeconds > $internalMs) {
331*04fd306cSNickeau            return true;
332*04fd306cSNickeau        }
333*04fd306cSNickeau        return false;
334*04fd306cSNickeau
335*04fd306cSNickeau    }
336*04fd306cSNickeau
337*04fd306cSNickeau    public function diff(DateTime $rightTime): \DateInterval
338*04fd306cSNickeau    {
339*04fd306cSNickeau        // example get the s part of the diff (even if there is day of diff)
340*04fd306cSNickeau        // $seconds = $diff->format('%s');
341*04fd306cSNickeau        return $this->dateTime->diff($rightTime, true);
342*04fd306cSNickeau    }
343c3437056SNickeau
34437748cd8SNickeau
34537748cd8SNickeau}
346