diff --git a/examples/async/usleep.php b/examples/async/usleep.php index 7c433847..622317dc 100644 --- a/examples/async/usleep.php +++ b/examples/async/usleep.php @@ -25,7 +25,7 @@ $duration = DateTime\Timestamp::now()->since($start); - IO\write_error_line("duration: %s.", $duration->toString(max_decimals: 2)); + IO\write_error_line("duration : %s.", $duration->toString(max_decimals: 5)); return 0; }); diff --git a/sample.php b/sample.php index 1c97b20e..b42b084b 100644 --- a/sample.php +++ b/sample.php @@ -17,7 +17,8 @@ ); IO\write_line('The offset of the timezone: %s', $someday->getTimezone()->getOffset($someday)->getTotalMinutes()); - IO\write_line('The location of the timezone: %s', json_encode($someday->getTimezone()->getLocation())); + IO\write_line('The raw offset of the timezone: %s', $someday->getTimezone()->getRawOffset()->getTotalMinutes()); + IO\write_line('The dst savings of the timezone: %s', $someday->getTimezone()->getDaylightSavingTimeSavings()->getTotalMinutes()); $today = DateTime\DateTime::now(DateTime\Timezone::EuropeLondon); $today_native = new NativeDateTime('now', new NativeDateTimeZone('Europe/London')); @@ -25,14 +26,14 @@ $that_day = $today->plusDays(24); $that_day_native = $today_native->modify('+24 days'); - var_dump($that_day->format(DateTime\DateFormat::Iso8601), $that_day_native->format(DateTimeInterface::ATOM)); + var_dump($that_day->format(DateTime\DatePattern::Iso8601), $that_day_native->format(DateTimeInterface::ATOM)); var_dump($that_day->getTimezone()->name, $that_day_native->getTimezone()->getName()); var_dump($that_day->getTimezone()->getOffset($that_day), $that_day_native->getTimezone()->getOffset($that_day_native)); $now = DateTime\DateTime::now(DateTime\Timezone::EuropeLondon); - foreach (DateTime\DateFormat::cases() as $case) { - IO\write_line('time: %s -> %s', $case->name, $now->format(format: $case, locale: Locale\Locale::English)); + foreach (DateTime\DatePattern::cases() as $case) { + IO\write_line('time: %s -> %s', $case->name, $now->format(pattern: $case, locale: Locale\Locale::English)); } IO\write_line('The offset of the timezone: %s', $now->getTimezone()->getOffset($now)->toString()); @@ -47,8 +48,11 @@ var_dump($now_timestamp->compare($future_timestamp), $future->isAfter($now), $now->isBeforeOrAtTheSameTime($future)); + IO\write_line('Does the timezone uses dst? %s', $future->getTimezone()->usesDaylightSavingTime() ? 'Yes' : 'No'); + IO\write_line('The raw offset of the future timezone: %s', $future->getTimezone()->getRawOffset()->toString()); IO\write_line('The offset of the future timezone: %s', $future->getTimezone()->getOffset($future)->toString()); - IO\write_line('Time: %s', $now->getTimestamp()->format($now->getTimezone(), locale: Locale\Locale::French)); + IO\write_line('The dst offset of the future timezone: %s', $future->getTimezone()->getDaylightSavingTimeOffset($future)->toString()); + IO\write_line('Time: %s', $now->getTimestamp()->format(timezone: $now->getTimezone(), locale: Locale\Locale::French)); $now = DateTime\DateTime::now(DateTime\Timezone::EuropeLondon); IO\write_line('Time: %s', json_encode($now)); diff --git a/src/Psl/DateTime/DateFormat.php b/src/Psl/DateTime/DatePattern.php similarity index 76% rename from src/Psl/DateTime/DateFormat.php rename to src/Psl/DateTime/DatePattern.php index 09ee71da..c3ac97ce 100644 --- a/src/Psl/DateTime/DateFormat.php +++ b/src/Psl/DateTime/DatePattern.php @@ -5,12 +5,12 @@ namespace Psl\DateTime; /** - * An enumeration of common date format strings. + * An enum of common date pattern strings. * - * This enum provides a collection of standardized date format strings for various protocols + * This enum provides a collection of standardized date pattern strings for various protocols * and standards, such as RFC 2822, ISO 8601, HTTP headers, and more. */ -enum DateFormat: string +enum DatePattern: string { case Rfc2822 = 'EEE, dd MMM yyyy HH:mm:ss Z'; case Iso8601 = 'yyyy-MM-dd\'T\'HH:mm:ssXXX'; diff --git a/src/Psl/DateTime/DateTime.php b/src/Psl/DateTime/DateTime.php index a0c096b7..d8934786 100644 --- a/src/Psl/DateTime/DateTime.php +++ b/src/Psl/DateTime/DateTime.php @@ -4,10 +4,8 @@ namespace Psl\DateTime; -use Psl\Str; - -use function getdate; -use function mktime; +use IntlCalendar; +use Psl\Locale\Locale; final class DateTime implements DateTimeInterface { @@ -87,24 +85,16 @@ private function __construct(Timezone $timezone, Timestamp $timestamp, int $year } /** - * Creates a new DateTime instance representing the current moment. - * - * This static method returns a DateTime object set to the current date and time. If a specific timezone is - * provided, the returned DateTime will be adjusted to reflect the date and time in that timezone. If no timezone - * is specified, the system's default timezone is used. + * Creates a new {@see DateTime} instance representing the current moment. * - * Utilizing the {@see Timestamp::now()} method, this function captures the precise moment of invocation down to the - * nanosecond. This ensures that the {@see DateTime] instance represents an accurate and precise point in time. - * - * The method optionally accepts a timezone. If omitted, the DateTime object is created using the default system - * timezone, as determined by {@see Timezone::default()}. This flexibility allows for the creation of DateTime instances - * that are relevant in various global contexts without the need for manual timezone conversion by the caller. + * This static method returns a {@see DateTime} object set to the current date and time. If a specific timezone is + * provided, the returned {@see DateTime} will be adjusted to reflect the date and time in that timezone. * * @pure */ - public static function now(?Timezone $timezone = null): DateTime + public static function now(Timezone $timezone): DateTime { - return self::fromTimestamp(Timestamp::now(), $timezone ?? Timezone::default()); + return self::fromTimestamp(Timestamp::now(), $timezone); } /** @@ -115,9 +105,6 @@ public static function now(?Timezone $timezone = null): DateTime * method combines the current date context with a specific time, offering a convenient way to specify times such * as "today at 14:00" in code. * - * If a timezone is provided, the {@see DateTime} object is adjusted to reflect the specified time in that timezone. If - * the timezone parameter is omitted, the system's default timezone, as determined by {@see Timezone::default()}, is used. - * * The time components (hours, minutes, seconds, nanoseconds) must be within their valid ranges. The method * enforces these constraints and throws an {@see Exception\InvalidArgumentException} if any component is out of bounds. * @@ -131,7 +118,7 @@ public static function now(?Timezone $timezone = null): DateTime * * @pure */ - public static function todayAt(int $hours, int $minutes, int $seconds = 0, int $nanoseconds = 0, ?Timezone $timezone = null): DateTime + public static function todayAt(Timezone $timezone, int $hours, int $minutes, int $seconds = 0, int $nanoseconds = 0): DateTime { return self::now($timezone)->withTime($hours, $minutes, $seconds, $nanoseconds); } @@ -139,11 +126,7 @@ public static function todayAt(int $hours, int $minutes, int $seconds = 0, int $ /** * Creates a {@see DateTime} instance from individual date and time components. * - * This method constructs a DateTime object based on the specified year, month, day, hour, minute, second, and nanosecond, - * taking into account the specified or default timezone. It validates the date and time components to ensure they form - * a valid date-time. If the components are invalid (e.g., 30th February), an {@see Exception\InvalidArgumentException} is thrown. - * - * In cases where the specified time occurs twice (such as during the end of daylight saving time), the earlier occurrence + * Note: In cases where the specified time occurs twice (such as during the end of daylight saving time), the earlier occurrence * is returned. To obtain the later occurrence, you can adjust the returned instance using `->plusHours(1)`. * * @param Month|int<1, 12> $month @@ -159,96 +142,113 @@ public static function todayAt(int $hours, int $minutes, int $seconds = 0, int $ */ public static function fromParts(int $year, Month|int $month, int $day, int $hours = 0, int $minutes = 0, int $seconds = 0, int $nanoseconds = 0, Timezone $timezone = null): self { - $timezone ??= Timezone::default(); + if ($month instanceof Month) { + $month = $month->value; + } - return Internal\zone_override($timezone, static function () use ($timezone, $year, $month, $day, $hours, $minutes, $seconds, $nanoseconds): DateTime { - if ($month instanceof Month) { - $month = $month->value; - } + /** @var IntlCalendar $calendar */ + $calendar = IntlCalendar::createInstance( + $timezone === null ? null : Internal\to_intl_timezone($timezone), + ); - $timestamp = Timestamp::fromRaw( - mktime($hours, $minutes, $seconds, $month, $day, $year), - $nanoseconds, - ); + $calendar->set($year, $month - 1, $day, $hours, $minutes, $seconds); - $ret = new DateTime( - $timezone, - $timestamp, - $year, - $month, - $day, - $hours, - $minutes, - $seconds, - $nanoseconds, + // Validate the date-time components by comparing them to what was set + if ( + !($calendar->get(IntlCalendar::FIELD_YEAR) === $year && + ($calendar->get(IntlCalendar::FIELD_MONTH) + 1) === $month && + $calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH) === $day && + $calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY) === $hours && + $calendar->get(IntlCalendar::FIELD_MINUTE) === $minutes && + $calendar->get(IntlCalendar::FIELD_SECOND) === $seconds) + ) { + throw new Exception\InvalidArgumentException( + 'The given components do not form a valid date-time.', ); + } + + $timestampInSeconds = (int) ($calendar->getTime() / MILLISECONDS_PER_SECOND); + $timestamp = Timestamp::fromRaw($timestampInSeconds, $nanoseconds); - // mktime() doesn't throw on invalid date/time, but silently returns a - // timestamp that doesn't match the input; so we check for that here. - if ($ret->getParts() !== DateTime::fromTimestamp($timestamp, $timezone)->getParts()) { - throw new Exception\InvalidArgumentException(Str\format( - 'The given components do not form a valid date-time in the timezone "%s"', - $timezone->value, - )); - } - - return $ret; - }); + return new self( + $timezone, + $timestamp, + $year, + $month, + $day, + $hours, + $minutes, + $seconds, + $nanoseconds + ); } /** * Creates a {@see DateTime} instance from a timestamp, representing the same point in time. * * This method converts a {@see Timestamp} into a {@see DateTime} instance calculated for the specified timezone. - * It extracts and uses the date and time parts corresponding to the given timestamp in the provided or default timezone. - * - * @param Timezone|null $timezone The timezone for the DateTime instance. Defaults to the system's default timezone if null. * * @pure */ - public static function fromTimestamp(Timestamp $timestamp, ?Timezone $timezone = null): DateTime + public static function fromTimestamp(Timestamp $timestamp, Timezone $timezone): DateTime { - $timezone ??= Timezone::default(); - - return Internal\zone_override($timezone, static function () use ($timezone, $timestamp): DateTime { - [$s, $ns] = $timestamp->toRaw(); - $parts = getdate($s); - - return new static( - $timezone, - $timestamp, - $parts['year'], - $parts['mon'], - $parts['mday'], - $parts['hours'], - $parts['minutes'], - $parts['seconds'], - $ns, - ); - }); + /** @var IntlCalendar $calendar */ + $calendar = IntlCalendar::createInstance( + Internal\to_intl_timezone($timezone), + ); + + $calendar->setTime( + $timestamp->getSeconds() * MILLISECONDS_PER_SECOND, + ); + + $year = $calendar->get(IntlCalendar::FIELD_YEAR); + $month = $calendar->get(IntlCalendar::FIELD_MONTH) + 1; + $day = $calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH); + $hour = $calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY); + $minute = $calendar->get(IntlCalendar::FIELD_MINUTE); + $second = $calendar->get(IntlCalendar::FIELD_SECOND); + $nanoseconds = $timestamp->getNanoseconds(); + + return new static( + $timezone, + $timestamp, + $year, + $month, + $day, + $hour, + $minute, + $second, + $nanoseconds, + ); } /** - * Parses a date-time string and returns a {@see DateTime} instance for the specified or default timezone. + * Creates a {@see DateTime} instance from a date/time string according to a specific pattern. * - * This method interprets a date-time string, potentially relative (e.g., "next Thursday", "10 minutes ago") or absolute - * (e.g., "2023-03-15 12:00:00"), into a {@see DateTime} object. An optional base time (`$relative_to`) can be provided to resolve - * relative date-time expressions. If `$relative_to` is not provided, the current system time is used as the base. + * This method allows parsing of date/time strings that conform to custom patterns, + * making it versatile for handling various date/time formats. * - * @param string $raw_string The date-time string to parse. - * @param Timezone|null $timezone The timezone to use for the resulting DateTime instance. Defaults to the system's default timezone if null. - * @param TemporalInterface|null $relative_to The temporal context used to interpret relative date-time expressions. Defaults to the current system time if null. + * @throws Exception\RuntimeException If parsing fails or the date/time string is invalid. + */ + public static function fromPattern(string|DatePattern $pattern, string $raw_string, Timezone $timezone, ?Locale $locale = null): self + { + return self::fromTimestamp( + Timestamp::fromPattern($pattern, $raw_string, $timezone, $locale), + $timezone, + ); + } + + /** + * Parses a date/time string into a {@see DateTime} instance. * - * @throws Exception\InvalidArgumentException If parsing fails or the date-time string is invalid. + * This method is a convenience wrapper for parsing date/time strings without specifying a custom pattern. * - * @see https://www.php.net/manual/en/datetime.formats.php For information on supported date and time formats. + * @throws Exception\RuntimeException If parsing fails or the format of the date/time string is invalid. */ - public static function parse(string $raw_string, ?Timezone $timezone = null, ?TemporalInterface $relative_to = null): self + public static function parse(string $raw_string, Timezone $timezone, ?Locale $locale = null): self { - $timezone ??= Timezone::default(); - return self::fromTimestamp( - Timestamp::parse($raw_string, $timezone, $relative_to), + Timestamp::parse($raw_string, $timezone, $locale), $timezone, ); } diff --git a/src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php b/src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php index 480e133a..d63e84be 100644 --- a/src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php +++ b/src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php @@ -4,16 +4,10 @@ namespace Psl\DateTime; -use DateTime as NativeDateTime; -use DateTimeZone as NativeDateTimeZone; -use IntlDateFormatter; use Psl\DateTime; use Psl\Locale; use Psl\Math; -use function date; -use function Psl\DateTime; - /** * @require-implements DateTimeInterface */ @@ -421,9 +415,7 @@ public function getWeekday(): Weekday */ public function isDaylightSavingTime(): bool { - return Internal\zone_override($this->getTimezone(), function (): bool { - return date('I', $this->getTimestamp()->getSeconds()) === '1'; - }); + return !$this->getTimezone()->getDaylightSavingTimeOffset($this)->isZero(); } /** @@ -618,25 +610,21 @@ public function minusDays(int $days): static } /** - * Formats the date and time according to the specified format and locale. + * Formats the date and time of this instance into a string based on the provided pattern, timezone, and locale. + * + * If no pattern is specified, a default pattern will be used. + * + * The method uses the associated timezone of the instance by default, but this can be overridden with the + * provided timezone parameter. * - * @param DateFormat|string|null $format The format string or {@see DateFormat}. If null, the default format is used. - * @param Locale\Locale|null $locale The locale for formatting. If null, the default locale is used. + * The method also accounts for locale-specific formatting rules if a locale is provided. * * @mutation-free + * + * @note The default pattern is subject to change at any time and should not be relied upon for consistent formatting. */ - public function format(DateFormat|string|null $format = null, ?Locale\Locale $locale = null): string + public function format(DatePattern|string|null $pattern = null, ?Timezone $timezone = null, ?Locale\Locale $locale = null): string { - return Internal\zone_override($this->getTimezone(), function () use ($locale, $format): string { - $obj = new NativeDateTime(); - $obj->setTimestamp($this->getTimestamp()->getSeconds()); - $obj->setTimezone(new NativeDateTimeZone($this->getTimezone()->value)); - - if ($format instanceof DateFormat) { - $format = $format->value; - } - - return IntlDateFormatter::formatObject($obj, $format, $locale?->value); - }); + return $this->getTimestamp()->format($pattern, $timezone ?? $this->getTimezone(), $locale); } } diff --git a/src/Psl/DateTime/DateTimeInterface.php b/src/Psl/DateTime/DateTimeInterface.php index da4bb11b..db8ec4c3 100644 --- a/src/Psl/DateTime/DateTimeInterface.php +++ b/src/Psl/DateTime/DateTimeInterface.php @@ -398,12 +398,18 @@ public function minusDays(int $days): static; public function convertToTimezone(Timezone $timezone): static; /** - * Formats the date and time according to the specified format and locale. + * Formats the date and time of this instance into a string based on the provided pattern, timezone, and locale. * - * @param DateFormat|string|null $format The format string or {@see DateFormat}. If null, the default format is used. - * @param Locale\Locale|null $locale The locale for formatting. If null, the default locale is used. + * If no pattern is specified, a default pattern will be used. + * + * The method uses the associated timezone of the instance by default, but this can be overridden with the + * provided timezone parameter. + * + * The method also accounts for locale-specific formatting rules if a locale is provided. * * @mutation-free + * + * @note The default pattern is subject to change at any time and should not be relied upon for consistent formatting. */ - public function format(DateFormat|string|null $format = null, ?Locale\Locale $locale = null): string; + public function format(DatePattern|string|null $pattern = null, ?Timezone $timezone = null, ?Locale\Locale $locale = null): string; } diff --git a/src/Psl/DateTime/Exception/RuntimeException.php b/src/Psl/DateTime/Exception/RuntimeException.php new file mode 100644 index 00000000..37fdc9f3 --- /dev/null +++ b/src/Psl/DateTime/Exception/RuntimeException.php @@ -0,0 +1,11 @@ +value; + if (Byte\starts_with($value, '+') || Byte\starts_with($value, '-')) { + $value = 'GMT' . $value; + } + + return IntlTimeZone::createTimeZone($value); +} diff --git a/src/Psl/DateTime/Internal/zone_override.php b/src/Psl/DateTime/Internal/zone_override.php deleted file mode 100644 index 674a7156..00000000 --- a/src/Psl/DateTime/Internal/zone_override.php +++ /dev/null @@ -1,39 +0,0 @@ -value); - } - - return $callback(); - } finally { - if ($original !== null) { - date_default_timezone_set($original); - } - } -} diff --git a/src/Psl/DateTime/TemporalInterface.php b/src/Psl/DateTime/TemporalInterface.php index 755cdab4..311ec362 100644 --- a/src/Psl/DateTime/TemporalInterface.php +++ b/src/Psl/DateTime/TemporalInterface.php @@ -7,6 +7,7 @@ use JsonSerializable; use Psl\Comparison\Comparable; use Psl\Comparison\Equable; +use Psl\Locale; /** * Represents a temporal object that can be manipulated and compared. @@ -202,4 +203,19 @@ public function since(TemporalInterface $other): Duration; * @mutation-free */ public function convertToTimezone(Timezone $timezone): DateTimeInterface; + + /** + * Formats the date and time of this instance into a string based on the provided pattern, timezone, and locale. + * + * If no pattern is specified, a default pattern will be used. + * + * If no timezone is specified, {@see Timezone::UTC} will be used. + * + * The method also accounts for locale-specific formatting rules if a locale is provided. + * + * @mutation-free + * + * @note The default pattern is subject to change at any time and should not be relied upon for consistent formatting. + */ + public function format(DatePattern|string|null $pattern = null, ?Timezone $timezone = null, ?Locale\Locale $locale = null): string; } diff --git a/src/Psl/DateTime/Timestamp.php b/src/Psl/DateTime/Timestamp.php index 5f6a582f..be436361 100644 --- a/src/Psl/DateTime/Timestamp.php +++ b/src/Psl/DateTime/Timestamp.php @@ -4,14 +4,11 @@ namespace Psl\DateTime; -use DateTime as NativeDateTime; -use DateTimeZone as NativeDateTimeZone; use IntlDateFormatter; use Psl\Locale; use Psl\Math; use function hrtime; -use function strtotime; use function time; /** @@ -19,7 +16,7 @@ */ final class Timestamp implements TemporalInterface { - use \Psl\DateTime\TemporalConvenienceMethodsTrait; + use TemporalConvenienceMethodsTrait; /** * @var null|array{seconds: int, nanoseconds: int} @@ -111,41 +108,62 @@ public static function fromRaw(int $seconds, int $nanoseconds = 0): Timestamp return new self($finalSeconds, $normalizedNanoseconds); } - /** - * Parses a date/time string into an instance, considering the provided timezone. - * - * If the `$raw_string` includes a timezone, it overrides the `$timezone` argument. The - * `$relative_to` argument is for parsing relative time strings, defaulting to the current - * time if not specified. + * Creates a {@see Timestamp} instance from a date/time string according to a specific pattern. * - * @param ?Timezone $timezone The timezone context for parsing. Defaults to the system's timezone. - * @param ?TemporalInterface $relative_to Context for relative time strings. + * This method allows parsing of date/time strings that conform to custom patterns, + * making it versatile for handling various date/time formats. * - * @throws Exception\InvalidArgumentException If parsing fails or the format is invalid. + * @throws Exception\RuntimeException If parsing fails or the date/time string is invalid. + */ + public static function fromPattern(string|DatePattern $pattern, string $raw_string, ?Timezone $timezone = null, ?Locale\Locale $locale = null): self + { + $pattern = $pattern instanceof DatePattern ? $pattern->value : $pattern; + + $formatter = new IntlDateFormatter( + $locale?->value, + IntlDateFormatter::FULL, + IntlDateFormatter::FULL, + $timezone === null ? null : Internal\to_intl_timezone($timezone), + IntlDateFormatter::GREGORIAN, + $pattern, + ); + + $timestamp = $formatter->parse($raw_string, $offset); + if ($timestamp === false) { + throw new Exception\RuntimeException( + "Parsing error: Unable to interpret '$raw_string' as a valid date/time using pattern '$pattern'.", + ); + } + + return self::fromRaw($timestamp); + } + + /** + * Parses a date/time string into a {@see Timestamp} instance. * - * @see https://www.php.net/manual/en/datetime.formats.php + * This method is a convenience wrapper for parsing date/time strings without specifying a custom pattern. * - * @pure + * @throws Exception\RuntimeException If parsing fails or the format of the date/time string is invalid. */ - public static function parse(string $raw_string, ?Timezone $timezone = null, ?TemporalInterface $relative_to = null): self + public static function parse(string $raw_string, ?Timezone $timezone = null, ?Locale\Locale $locale = null): self { - $timezone ??= Timezone::default(); - - return Internal\zone_override($timezone, static function () use ($raw_string, $relative_to): self { - if ($relative_to !== null) { - $relative_to = $relative_to->getTimestamp()->getSeconds(); - } - - $raw = strtotime($raw_string, $relative_to); - if ($raw === false) { - throw new Exception\InvalidArgumentException( - "Failed to parse the provided date/time string: '$raw_string'", - ); - } - - return self::fromRaw($raw); - }); + $formatter = new IntlDateFormatter( + $locale?->value, + IntlDateFormatter::FULL, + IntlDateFormatter::FULL, + $timezone === null ? null : Internal\to_intl_timezone($timezone), + IntlDateFormatter::GREGORIAN, + ); + + $timestamp = $formatter->parse($raw_string); + if ($timestamp === false) { + throw new Exception\RuntimeException( + "Parsing error: Unable to interpret '$raw_string' as a valid date/time.", + ); + } + + return self::fromRaw($timestamp); } /** @@ -233,27 +251,34 @@ public function minus(Duration $duration): static } /** - * Formats the timestamp according to the given format and timezone. + * Formats the date and time of this instance into a string based on the provided pattern, timezone, and locale. * - * The format can be a predefined {@see DateFormat} case, a date-time format string, or `null` for default formatting. + * If no pattern is specified, a default pattern will be used. * - * The timezone is required to accurately represent the timestamp in the desired locale. + * If no timezone is specified, {@see Timezone::UTC} will be used. + * + * The method also accounts for locale-specific formatting rules if a locale is provided. * * @mutation-free + * + * @note The default pattern is subject to change at any time and should not be relied upon for consistent formatting. */ - public function format(Timezone $timezone, null|DateFormat|string $format = null, ?Locale\Locale $locale = null): string + public function format(null|DatePattern|string $pattern = null, ?Timezone $timezone = null, ?Locale\Locale $locale = null): string { - return Internal\zone_override($timezone, function () use ($timezone, $locale, $format): string { - $obj = new NativeDateTime(); - $obj->setTimestamp($this->getSeconds()); - $obj->setTimezone(new NativeDateTimeZone($timezone->value)); + if ($pattern instanceof DatePattern) { + $pattern = $pattern->value; + } - if ($format instanceof DateFormat) { - $format = $format->value; - } + $formatter = new IntlDateFormatter( + $locale?->value, + IntlDateFormatter::LONG, + IntlDateFormatter::LONG, + $timezone === null ? null : Internal\to_intl_timezone($timezone), + IntlDateFormatter::GREGORIAN, + $pattern, + ); - return IntlDateFormatter::formatObject($obj, $format, $locale->value); - }); + return $formatter->format($this->getSeconds()); } public function jsonSerialize(): array diff --git a/src/Psl/DateTime/Timezone.php b/src/Psl/DateTime/Timezone.php index 1a224ab0..8f21def9 100644 --- a/src/Psl/DateTime/Timezone.php +++ b/src/Psl/DateTime/Timezone.php @@ -4,8 +4,6 @@ namespace Psl\DateTime; -use DateTimeZone; - /** * Enumerates all supported time zones, including UTC, all tzdata time zones, and all unique UTC offsets. * @@ -162,8 +160,8 @@ enum Timezone : string case AmericaDominica = 'America/Dominica'; case AmericaEdmonton = 'America/Edmonton'; case AmericaEirunepe = 'America/Eirunepe'; - case AmericaEl_Salvador = 'America/El_Salvador'; - case AmericaFort_Nelson = 'America/Fort_Nelson'; + case AmericaElSalvador = 'America/El_Salvador'; + case AmericaFortNelson = 'America/Fort_Nelson'; case AmericaFortaleza = 'America/Fortaleza'; case AmericaGlaceBay = 'America/Glace_Bay'; case AmericaGooseBay = 'America/Goose_Bay'; @@ -194,7 +192,7 @@ enum Timezone : string case AmericaLaPaz = 'America/La_Paz'; case AmericaLima = 'America/Lima'; case AmericaLosAngeles = 'America/Los_Angeles'; - case AmericaLower_Princes = 'America/Lower_Princes'; + case AmericaLowerPrinces = 'America/Lower_Princes'; case AmericaMaceio = 'America/Maceio'; case AmericaManagua = 'America/Managua'; case AmericaManaus = 'America/Manaus'; @@ -212,7 +210,7 @@ enum Timezone : string case AmericaMontevideo = 'America/Montevideo'; case AmericaMontserrat = 'America/Montserrat'; case AmericaNassau = 'America/Nassau'; - case AmericaNew_York = 'America/New_York'; + case AmericaNewYork = 'America/New_York'; case AmericaNipigon = 'America/Nipigon'; case AmericaNome = 'America/Nome'; case AmericaNoronha = 'America/Noronha'; @@ -235,10 +233,10 @@ enum Timezone : string case AmericaRecife = 'America/Recife'; case AmericaRegina = 'America/Regina'; case AmericaResolute = 'America/Resolute'; - case AmericaRio_Branco = 'America/Rio_Branco'; + case AmericaRioBranco = 'America/Rio_Branco'; case AmericaSantarem = 'America/Santarem'; case AmericaSantiago = 'America/Santiago'; - case AmericaSanto_Domingo = 'America/Santo_Domingo'; + case AmericaSantoDomingo = 'America/Santo_Domingo'; case AmericaSaoPaulo = 'America/Sao_Paulo'; case AmericaScoresbysund = 'America/Scoresbysund'; case AmericaSitka = 'America/Sitka'; @@ -300,7 +298,7 @@ enum Timezone : string case AsiaGaza = 'Asia/Gaza'; case AsiaHebron = 'Asia/Hebron'; case AsiaHoChiMinh = 'Asia/Ho_Chi_Minh'; - case AsiaHong_Kong = 'Asia/Hong_Kong'; + case AsiaHongKong = 'Asia/Hong_Kong'; case AsiaHovd = 'Asia/Hovd'; case AsiaIrkutsk = 'Asia/Irkutsk'; case AsiaJakarta = 'Asia/Jakarta'; @@ -358,7 +356,7 @@ enum Timezone : string case AtlanticAzores = 'Atlantic/Azores'; case AtlanticBermuda = 'Atlantic/Bermuda'; case AtlanticCanary = 'Atlantic/Canary'; - case AtlanticCape_Verde = 'Atlantic/Cape_Verde'; + case AtlanticCapeVerde = 'Atlantic/Cape_Verde'; case AtlanticFaroe = 'Atlantic/Faroe'; case AtlanticMadeira = 'Atlantic/Madeira'; case AtlanticReykjavik = 'Atlantic/Reykjavik'; @@ -416,7 +414,7 @@ enum Timezone : string case EuropeRiga = 'Europe/Riga'; case EuropeRome = 'Europe/Rome'; case EuropeSamara = 'Europe/Samara'; - case EuropeSan_Marino = 'Europe/San_Marino'; + case EuropeSanMarino = 'Europe/San_Marino'; case EuropeSarajevo = 'Europe/Sarajevo'; case EuropeSaratov = 'Europe/Saratov'; case EuropeSimferopol = 'Europe/Simferopol'; @@ -473,11 +471,11 @@ enum Timezone : string case PacificNiue = 'Pacific/Niue'; case PacificNorfolk = 'Pacific/Norfolk'; case PacificNoumea = 'Pacific/Noumea'; - case PacificPago_Pago = 'Pacific/Pago_Pago'; + case PacificPagoPago = 'Pacific/Pago_Pago'; case PacificPalau = 'Pacific/Palau'; case PacificPitcairn = 'Pacific/Pitcairn'; case PacificPohnpei = 'Pacific/Pohnpei'; - case PacificPort_Moresby = 'Pacific/Port_Moresby'; + case PacificPortMoresby = 'Pacific/Port_Moresby'; case PacificRarotonga = 'Pacific/Rarotonga'; case PacificSaipan = 'Pacific/Saipan'; case PacificTahiti = 'Pacific/Tahiti'; @@ -487,60 +485,94 @@ enum Timezone : string case PacificWallis = 'Pacific/Wallis'; /** - * Returns the default system time zone as a {@see Timezone} enum instance. + * Calculates the total time zone offset for a given {@see TemporalInterface} instance. + * + * This total offset includes both the raw timezone offset and any daylight saving time (DST) adjustments applicable at the temporal instance's time. * - * This method attempts to determine the current default time zone set in the system and return - * its corresponding {@see Timezone} enum instance. If the system's default time zone does not match any - * of the enum cases, UTC is returned as the default. + * @param bool $local Indicates whether the temporal object's time should be treated as local time (`true`) or as UTC time (`false`). * - * @return Timezone The system's default time zone as a {@see Timezone} enum instance or UTC if not found. + * @return Duration The total offset from UTC as a Duration instance, including any DST adjustments. + * + * @mutation-free */ - public static function default(): Timezone + public function getOffset(TemporalInterface $temporal, bool $local = false): Duration { - return self::tryFrom(date_default_timezone_get()) ?? self::UTC; + $intl_timezone = Internal\to_intl_timezone($this); + $timestamp_millis = $temporal->getTimestamp()->getSeconds() * MILLISECONDS_PER_SECOND; + $intl_timezone->getOffset($timestamp_millis, $local, $raw_offset, $dst_offset); + + return Duration::milliseconds($raw_offset + $dst_offset); } /** - * Calculates the time zone offset for a given {@see TemporalInterface} instance. + * Calculates the raw time zone offset for the current timezone, excluding any daylight saving time (DST) adjustments. * - * This method determines the UTC offset for the time zone, based on the specific date and time - * represented by the provided {@see TemporalInterface} instance. This is important because the offset - * can vary due to daylight saving time changes. + * This method retrieves the fixed offset from UTC for the timezone without considering any seasonal adjustments + * that might apply due to DST. It's particularly useful for understanding the base offset of a timezone. * - * @param TemporalInterface $temporal The temporal object to calculate the time zone offset for. + * @mutation-free + */ + public function getRawOffset(): Duration + { + return Duration::milliseconds(Internal\to_intl_timezone($this)->getRawOffset()); + } + + /** + * Calculates the daylight saving time (DST) offset for a given {@see TemporalInterface} instance at its specific time. + * + * This DST offset is the adjustment added to the raw timezone offset, if DST is in effect at the temporal instance's time. * - * @return Duration The offset from UTC as a Duration instance. + * @param bool $local Indicates whether the temporal object's time should be treated as local time (`true`) or as UTC time (`false`). + * + * @return Duration The DST offset as a Duration instance. If DST is not in effect, the offset will be zero. * * @mutation-free */ - public function getOffset(TemporalInterface $temporal): Duration + public function getDaylightSavingTimeOffset(TemporalInterface $temporal, bool $local = false): Duration { - return Internal\zone_override($this, static function () use ($temporal): Duration { - return Duration::seconds((int) date('Z', $temporal->getTimestamp()->getSeconds())); - }); + $intl_timezone = Internal\to_intl_timezone($this); + $timestamp_millis = $temporal->getTimestamp()->getSeconds() * MILLISECONDS_PER_SECOND; + $intl_timezone->getOffset($timestamp_millis, $local, $_, $dst_offset); + + return Duration::milliseconds($dst_offset); } /** - * Retrieves location information associated with this time zone. + * Determines whether the current timezone observes Daylight Saving Time (DST). * - * For time zones corresponding to a specific location, this method provides geographical - * information such as the country code, latitude, and longitude. This can be useful for - * applications needing to display location-based data or perform calculations based on - * geographical positions. + * This method checks if the timezone has any DST rules and if DST is applied at any point during the year. * - * @return TimezoneLocation|null Location information for the time zone, or null if the time zone does not - * correspond to a specific geographical location or if location data is unavailable. + * @return bool True if the timezone uses Daylight Saving Time at any point in the year, false otherwise. * * @mutation-free */ - public function getLocation(): ?TimezoneLocation + public function usesDaylightSavingTime(): bool { - $tz = new DateTimeZone($this->value); - $location = $tz->getLocation(); - if (!$location || '??' === $location['country_code']) { - return null; - } + return Internal\to_intl_timezone($this)->useDaylightTime(); + } - return new TimezoneLocation($location['country_code'], $location['latitude'], $location['longitude']); + /** + * Retrieves the amount of time added during Daylight Saving Time for the current timezone. + * + * This method returns the typical adjustment made to the local time when DST is in effect. + * + * If the timezone does not observe DST or if there is no current DST adjustment (e.g., outside of DST periods), + * the method will return a Duration of zero. + * + * @mutation-free + */ + public function getDaylightSavingTimeSavings(): Duration + { + return Duration::milliseconds(Internal\to_intl_timezone($this)->getDSTSavings()); + } + + /** + * Determines whether the current timezone has the same rules as another specified timezone. + * + * @mutation-free + */ + public function hasTheSameRulesAs(Timezone $other): bool + { + return Internal\to_intl_timezone($this)->hasSameRules(Internal\to_intl_timezone($other)); } } diff --git a/src/Psl/DateTime/TimezoneLocation.php b/src/Psl/DateTime/TimezoneLocation.php deleted file mode 100644 index 24ef0f68..00000000 --- a/src/Psl/DateTime/TimezoneLocation.php +++ /dev/null @@ -1,49 +0,0 @@ - $this->countryCode, - 'latitude' => $this->latitude, - 'longitude' => $this->longitude, - ]; - } -} diff --git a/src/Psl/Internal/Loader.php b/src/Psl/Internal/Loader.php index 13265464..69a9f138 100644 --- a/src/Psl/Internal/Loader.php +++ b/src/Psl/Internal/Loader.php @@ -537,7 +537,7 @@ final class Loader 'Psl\\Range\\between' => 'Psl/Range/between.php', 'Psl\\Range\\full' => 'Psl/Range/full.php', 'Psl\\DateTime\\is_leap_year' => 'Psl/DateTime/is_leap_year.php', - 'Psl\\DateTime\\Internal\\zone_override' => 'Psl/DateTime/Internal/zone_override.php', + 'Psl\\DateTime\\Internal\\to_intl_timezone' => 'Psl/DateTime/Internal/to_intl_timezone.php', ]; public const INTERFACES = [ @@ -839,11 +839,11 @@ final class Loader 'Psl\\Range\\FullRange' => 'Psl/Range/FullRange.php', 'Psl\\DateTime\\Exception\\InvalidArgumentException' => 'Psl/DateTime/Exception/InvalidArgumentException.php', 'Psl\\DateTime\\Exception\\OverflowException' => 'Psl/DateTime/Exception/OverflowException.php', + 'Psl\\DateTime\\Exception\\RuntimeException' => 'Psl/DateTime/Exception/RuntimeException.php', 'Psl\\DateTime\\Exception\\UnderflowException' => 'Psl/DateTime/Exception/UnderflowException.php', 'Psl\\DateTime\\DateTime' => 'Psl/DateTime/DateTime.php', 'Psl\\DateTime\\Duration' => 'Psl/DateTime/Interval.php', 'Psl\\DateTime\\Timestamp' => 'Psl/DateTime/Timestamp.php', - 'Psl\\DateTime\\TimezoneLocation' => 'Psl/DateTime/TimezoneLocation.php', ]; public const ENUMS = [ @@ -860,7 +860,7 @@ final class Loader 'Psl\\Password\\Algorithm' => 'Psl/Password/Algorithm.php', 'Psl\\Shell\\ErrorOutputBehavior' => 'Psl/Shell/ErrorOutputBehavior.php', 'Psl\\Locale\\Locale' => 'Psl/Locale/Locale.php', - 'Psl\\DateTime\\DateFormat' => 'Psl/DateTime/DateFormat.php', + 'Psl\\DateTime\\DatePattern' => 'Psl/DateTime/DateFormat.php', 'Psl\\DateTime\\Era' => 'Psl/DateTime/Era.php', 'Psl\\DateTime\\Meridiem' => 'Psl/DateTime/Meridiem.php', 'Psl\\DateTime\\Month' => 'Psl/DateTime/Weekday.php',