xref: /template/strap/ComboStrap/Iso8601Date.php (revision 313de40a7a81adb8606d463d69a8d40c7499c8f8)
137748cd8SNickeau<?php
237748cd8SNickeau
337748cd8SNickeau
437748cd8SNickeaunamespace ComboStrap;
537748cd8SNickeau
637748cd8SNickeau
737748cd8SNickeauuse DateTime;
804fd306cSNickeauuse 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";
2304fd306cSNickeau    public const TIME_FORMATTER_TYPE = IntlDateFormatter::NONE;
2404fd306cSNickeau    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    /**
6304fd306cSNickeau     * @param $dateString
64c3437056SNickeau     * @return Iso8601Date
6504fd306cSNickeau     * @throws ExceptionBadSyntax if the format is not supported
66c3437056SNickeau     */
6704fd306cSNickeau    public static function createFromString(string $dateString): Iso8601Date
6837748cd8SNickeau    {
691fa8c418SNickeau
701fa8c418SNickeau        $original = $dateString;
711fa8c418SNickeau
7204fd306cSNickeau        $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) . ")";
13004fd306cSNickeau            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
15804fd306cSNickeau    /**
15904fd306cSNickeau     *
16004fd306cSNickeau     */
161c3437056SNickeau    public static function isValid($value): bool
162c3437056SNickeau    {
16304fd306cSNickeau        try {
164c3437056SNickeau            $dateObject = Iso8601Date::createFromString($value);
16504fd306cSNickeau            return $dateObject->isValidDateEntry(); // ??? Validation seems to be at construction
16604fd306cSNickeau        } catch (ExceptionBadSyntax $e) {
16704fd306cSNickeau            return false;
16804fd306cSNickeau        }
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
19004fd306cSNickeau    /**
19104fd306cSNickeau     * @throws ExceptionNotFound
19204fd306cSNickeau     */
19304fd306cSNickeau    public static function getInternationalFormatter($constant): int
19404fd306cSNickeau    {
19504fd306cSNickeau        $constantNormalized = trim(strtolower($constant));
19604fd306cSNickeau        switch ($constantNormalized) {
19704fd306cSNickeau            case "none":
19804fd306cSNickeau                return IntlDateFormatter::NONE;
19904fd306cSNickeau            case "full":
20004fd306cSNickeau                return IntlDateFormatter::FULL;
20104fd306cSNickeau            case "relativefull":
20204fd306cSNickeau                return IntlDateFormatter::RELATIVE_FULL;
20304fd306cSNickeau            case "long":
20404fd306cSNickeau                return IntlDateFormatter::LONG;
20504fd306cSNickeau            case "relativelong":
20604fd306cSNickeau                return IntlDateFormatter::RELATIVE_LONG;
20704fd306cSNickeau            case "medium":
20804fd306cSNickeau                return IntlDateFormatter::MEDIUM;
20904fd306cSNickeau            case "relativemedium":
21004fd306cSNickeau                return IntlDateFormatter::RELATIVE_MEDIUM;
21104fd306cSNickeau            case "short":
21204fd306cSNickeau                return IntlDateFormatter::SHORT;
21304fd306cSNickeau            case "relativeshort":
21404fd306cSNickeau                return IntlDateFormatter::RELATIVE_SHORT;
21504fd306cSNickeau            case "traditional":
21604fd306cSNickeau                return IntlDateFormatter::TRADITIONAL;
21704fd306cSNickeau            default:
21804fd306cSNickeau                throw new ExceptionNotFound("The constant ($constant) is not a valid constant", self::CANONICAL);
21904fd306cSNickeau        }
22004fd306cSNickeau    }
22104fd306cSNickeau
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
23204fd306cSNickeau    public function toIsoStringMs()
23304fd306cSNickeau    {
23404fd306cSNickeau        return $this->getDateTime()->format("Y-m-d\TH:i:s.u");
23504fd306cSNickeau    }
23604fd306cSNickeau
23737748cd8SNickeau    /**
23837748cd8SNickeau     * Shortcut to {@link DateTime::format()}
23904fd306cSNickeau     * 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
25404fd306cSNickeau    /**
25504fd306cSNickeau     * @throws ExceptionBadSyntax
25604fd306cSNickeau     */
25704fd306cSNickeau    public function formatLocale($pattern = null, $locale = null)
25804fd306cSNickeau    {
25904fd306cSNickeau        /**
26004fd306cSNickeau         * https://www.php.net/manual/en/function.strftime.php
26104fd306cSNickeau         * As been deprecated
26204fd306cSNickeau         * The only alternative with local is
26304fd306cSNickeau         * https://www.php.net/manual/en/intldateformatter.format.php
26404fd306cSNickeau         *
26504fd306cSNickeau         * Based on ISO date
26604fd306cSNickeau         * ICU Date formatter: https://unicode-org.github.io/icu-docs/#/icu4c/udat_8h.html
26704fd306cSNickeau         * ICU Date formats: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
26804fd306cSNickeau         * ICU User Guide: https://unicode-org.github.io/icu/userguide/
26904fd306cSNickeau         * ICU Formatting Dates and Times: https://unicode-org.github.io/icu/userguide/format_parse/datetime/
27004fd306cSNickeau         */
27104fd306cSNickeau        if (strpos($pattern, "%") !== false) {
27204fd306cSNickeau            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);
27304fd306cSNickeau            return strftime($pattern, $this->dateTime->getTimestamp());
27404fd306cSNickeau        }
27504fd306cSNickeau
27604fd306cSNickeau        /**
27704fd306cSNickeau         * This parameters
27804fd306cSNickeau         * are used to format date with the locale
27904fd306cSNickeau         * when the pattern is null
28004fd306cSNickeau         * Doc: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#producing-normal-date-formats-for-a-locale
28104fd306cSNickeau         *
28204fd306cSNickeau         * They may be null by the way.
28304fd306cSNickeau         *
28404fd306cSNickeau         */
28504fd306cSNickeau        $dateType = self::DATE_FORMATTER_TYPE;
28604fd306cSNickeau        $timeType = self::TIME_FORMATTER_TYPE;
28704fd306cSNickeau        if ($pattern !== null) {
28804fd306cSNickeau            $normalFormat = explode(" ", $pattern);
28904fd306cSNickeau            if (sizeof($normalFormat) === 2) {
29004fd306cSNickeau                try {
29104fd306cSNickeau                    $dateType = self::getInternationalFormatter($normalFormat[0]);
29204fd306cSNickeau                    $timeType = self::getInternationalFormatter($normalFormat[1]);
29304fd306cSNickeau                    $pattern = null;
29404fd306cSNickeau                } catch (ExceptionNotFound $e) {
29504fd306cSNickeau                    // ok
29604fd306cSNickeau                }
29704fd306cSNickeau            }
29804fd306cSNickeau        }
29904fd306cSNickeau
30004fd306cSNickeau        /**
30104fd306cSNickeau         * Formatter instantiation
302*313de40aSNicolas GERARD         * https://www.php.net/manual/en/intldateformatter.create.php
303*313de40aSNicolas GERARD         * List of local: with ResourceBundle::getLocales('')
30404fd306cSNickeau         */
305d8add9b6SNicolas GERARD        $intlDateFormatter = datefmt_create(
30604fd306cSNickeau            $locale,
30704fd306cSNickeau            $dateType,
30804fd306cSNickeau            $timeType,
30904fd306cSNickeau            $this->dateTime->getTimezone(),
31004fd306cSNickeau            IntlDateFormatter::GREGORIAN,
31104fd306cSNickeau            $pattern
31204fd306cSNickeau        );
313d8add9b6SNicolas GERARD        try {
314d8add9b6SNicolas GERARD            $formatted = datefmt_format($intlDateFormatter, $this->dateTime);
315d8add9b6SNicolas GERARD        } catch (\Error $e) {
316d8add9b6SNicolas GERARD            // Found unconstructed IntlDateFormatter
317d8add9b6SNicolas GERARD            // No idea how I can check that before formatting
318*313de40aSNicolas GERARD            LogUtility::warning("Locale value ($locale) is an unknown ICU locale. Using default locale instead", self::CANONICAL);
319d8add9b6SNicolas GERARD            $intlDateFormatter = datefmt_create(
320d8add9b6SNicolas GERARD                null,
321d8add9b6SNicolas GERARD                $dateType,
322d8add9b6SNicolas GERARD                $timeType,
323d8add9b6SNicolas GERARD                $this->dateTime->getTimezone(),
324d8add9b6SNicolas GERARD                IntlDateFormatter::GREGORIAN,
325d8add9b6SNicolas GERARD                $pattern
326d8add9b6SNicolas GERARD            );
327d8add9b6SNicolas GERARD            $formatted = datefmt_format($intlDateFormatter, $this->dateTime);
328d8add9b6SNicolas GERARD        }
32904fd306cSNickeau        if ($formatted === false) {
33004fd306cSNickeau            if ($locale === null) {
33104fd306cSNickeau                $locale = "";
33204fd306cSNickeau            }
33304fd306cSNickeau            if ($pattern === null) {
33404fd306cSNickeau                $pattern = "";
33504fd306cSNickeau            }
33604fd306cSNickeau            throw new ExceptionBadSyntax("Unable to format the date with the pattern ($pattern) and locale ($locale)");
33704fd306cSNickeau        }
33804fd306cSNickeau        return $formatted;
33904fd306cSNickeau    }
34004fd306cSNickeau
34104fd306cSNickeau    public function olderThan(DateTime $rightTime): bool
34204fd306cSNickeau    {
34304fd306cSNickeau
34404fd306cSNickeau        $internalMs = DataType::toMilliSeconds($this->dateTime);
34504fd306cSNickeau        $externalMilliSeconds = DataType::toMilliSeconds($rightTime);
34604fd306cSNickeau        if ($externalMilliSeconds > $internalMs) {
34704fd306cSNickeau            return true;
34804fd306cSNickeau        }
34904fd306cSNickeau        return false;
35004fd306cSNickeau
35104fd306cSNickeau    }
35204fd306cSNickeau
35304fd306cSNickeau    public function diff(DateTime $rightTime): \DateInterval
35404fd306cSNickeau    {
35504fd306cSNickeau        // example get the s part of the diff (even if there is day of diff)
35604fd306cSNickeau        // $seconds = $diff->format('%s');
35704fd306cSNickeau        return $this->dateTime->diff($rightTime, true);
35804fd306cSNickeau    }
359c3437056SNickeau
36037748cd8SNickeau
36137748cd8SNickeau}
362