Skip to content

Commit

Permalink
chore(datetime): remove usage of global state, use intl internally
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <azjezz@protonmail.com>
  • Loading branch information
azjezz committed Mar 22, 2024
1 parent 159e2c2 commit 2f0d41b
Show file tree
Hide file tree
Showing 14 changed files with 326 additions and 308 deletions.
2 changes: 1 addition & 1 deletion examples/async/usleep.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
14 changes: 9 additions & 5 deletions sample.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,23 @@
);

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'));

$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());
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
182 changes: 91 additions & 91 deletions src/Psl/DateTime/DateTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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.
*
Expand All @@ -131,19 +118,15 @@ 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);
}

/**
* 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
Expand All @@ -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,
);
}
Expand Down
36 changes: 12 additions & 24 deletions src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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);
}
}
Loading

0 comments on commit 2f0d41b

Please sign in to comment.