setTimezone( $to )->format( self::MYSQL ); } /** * Checking a string against a date format, strictly. * * PHP createFromFormat() accepts '2025-02-31' as 3 March, so the round trip is compared too. * * @param string $value Value to check. * @param string $format Expected format. * * @return bool Whether the value matches the format and denotes a real date. */ public static function matches_format( string $value, string $format ): bool { $date = DateTimeImmutable::createFromFormat( '!' . $format, $value, new DateTimeZone( 'UTC' ) ); if ( false === $date ) { return false; } $errors = DateTimeImmutable::getLastErrors(); if ( is_array( $errors ) && ( $errors['warning_count'] > 0 || $errors['error_count'] > 0 ) ) { return false; } return $date->format( $format ) === $value; } /** * Parsing a date and optional time into a UTC MySQL datetime. * * @param string $date Date in Kordiam's Y-m-d format. * @param string|null $time Time in Kordiam's H:i format, when the story has one. * * @return string|null UTC datetime, or null when the input is not a real date. */ public static function from_kordiam( string $date, ?string $time = null ): ?string { if ( ! self::matches_format( $date, self::KORDIAM_DATE ) ) { return null; } if ( null === $time || '' === trim( $time ) ) { $utc = self::local_to_utc( $date . ' ' . self::END_OF_DAY . ':00' ); return '' === $utc ? null : $utc; } if ( ! self::matches_format( $time, self::KORDIAM_TIME ) ) { return null; } $parsed = DateTimeImmutable::createFromFormat( '!' . self::KORDIAM_DATE . ' ' . self::KORDIAM_TIME, $date . ' ' . $time, new DateTimeZone( 'UTC' ) ); return false === $parsed ? null : $parsed->format( self::MYSQL ); } }