Skip to content

Commit 82398d0

Browse files
committed
chore(datetime): remove usage of global state, use intl internally
Signed-off-by: azjezz <azjezz@protonmail.com>
1 parent f01679f commit 82398d0

14 files changed

+326
-308
lines changed

examples/async/usleep.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
$duration = DateTime\Timestamp::now()->since($start);
2727

28-
IO\write_error_line("duration: %s.", $duration->toString(max_decimals: 2));
28+
IO\write_error_line("duration : %s.", $duration->toString(max_decimals: 5));
2929

3030
return 0;
3131
});

sample.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,23 @@
1717
);
1818

1919
IO\write_line('The offset of the timezone: %s', $someday->getTimezone()->getOffset($someday)->getTotalMinutes());
20-
IO\write_line('The location of the timezone: %s', json_encode($someday->getTimezone()->getLocation()));
20+
IO\write_line('The raw offset of the timezone: %s', $someday->getTimezone()->getRawOffset()->getTotalMinutes());
21+
IO\write_line('The dst savings of the timezone: %s', $someday->getTimezone()->getDaylightSavingTimeSavings()->getTotalMinutes());
2122

2223
$today = DateTime\DateTime::now(DateTime\Timezone::EuropeLondon);
2324
$today_native = new NativeDateTime('now', new NativeDateTimeZone('Europe/London'));
2425

2526
$that_day = $today->plusDays(24);
2627
$that_day_native = $today_native->modify('+24 days');
2728

28-
var_dump($that_day->format(DateTime\DateFormat::Iso8601), $that_day_native->format(DateTimeInterface::ATOM));
29+
var_dump($that_day->format(DateTime\DatePattern::Iso8601), $that_day_native->format(DateTimeInterface::ATOM));
2930
var_dump($that_day->getTimezone()->name, $that_day_native->getTimezone()->getName());
3031
var_dump($that_day->getTimezone()->getOffset($that_day), $that_day_native->getTimezone()->getOffset($that_day_native));
3132

3233
$now = DateTime\DateTime::now(DateTime\Timezone::EuropeLondon);
3334

34-
foreach (DateTime\DateFormat::cases() as $case) {
35-
IO\write_line('time: %s -> %s', $case->name, $now->format(format: $case, locale: Locale\Locale::English));
35+
foreach (DateTime\DatePattern::cases() as $case) {
36+
IO\write_line('time: %s -> %s', $case->name, $now->format(pattern: $case, locale: Locale\Locale::English));
3637
}
3738

3839
IO\write_line('The offset of the timezone: %s', $now->getTimezone()->getOffset($now)->toString());
@@ -47,8 +48,11 @@
4748

4849
var_dump($now_timestamp->compare($future_timestamp), $future->isAfter($now), $now->isBeforeOrAtTheSameTime($future));
4950

51+
IO\write_line('Does the timezone uses dst? %s', $future->getTimezone()->usesDaylightSavingTime() ? 'Yes' : 'No');
52+
IO\write_line('The raw offset of the future timezone: %s', $future->getTimezone()->getRawOffset()->toString());
5053
IO\write_line('The offset of the future timezone: %s', $future->getTimezone()->getOffset($future)->toString());
51-
IO\write_line('Time: %s', $now->getTimestamp()->format($now->getTimezone(), locale: Locale\Locale::French));
54+
IO\write_line('The dst offset of the future timezone: %s', $future->getTimezone()->getDaylightSavingTimeOffset($future)->toString());
55+
IO\write_line('Time: %s', $now->getTimestamp()->format(timezone: $now->getTimezone(), locale: Locale\Locale::French));
5256

5357
$now = DateTime\DateTime::now(DateTime\Timezone::EuropeLondon);
5458
IO\write_line('Time: %s', json_encode($now));

src/Psl/DateTime/DateFormat.php renamed to src/Psl/DateTime/DatePattern.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
namespace Psl\DateTime;
66

77
/**
8-
* An enumeration of common date format strings.
8+
* An enum of common date pattern strings.
99
*
10-
* This enum provides a collection of standardized date format strings for various protocols
10+
* This enum provides a collection of standardized date pattern strings for various protocols
1111
* and standards, such as RFC 2822, ISO 8601, HTTP headers, and more.
1212
*/
13-
enum DateFormat: string
13+
enum DatePattern: string
1414
{
1515
case Rfc2822 = 'EEE, dd MMM yyyy HH:mm:ss Z';
1616
case Iso8601 = 'yyyy-MM-dd\'T\'HH:mm:ssXXX';

src/Psl/DateTime/DateTime.php

Lines changed: 91 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44

55
namespace Psl\DateTime;
66

7-
use Psl\Str;
8-
9-
use function getdate;
10-
use function mktime;
7+
use IntlCalendar;
8+
use Psl\Locale\Locale;
119

1210
final class DateTime implements DateTimeInterface
1311
{
@@ -87,24 +85,16 @@ private function __construct(Timezone $timezone, Timestamp $timestamp, int $year
8785
}
8886

8987
/**
90-
* Creates a new DateTime instance representing the current moment.
91-
*
92-
* This static method returns a DateTime object set to the current date and time. If a specific timezone is
93-
* provided, the returned DateTime will be adjusted to reflect the date and time in that timezone. If no timezone
94-
* is specified, the system's default timezone is used.
88+
* Creates a new {@see DateTime} instance representing the current moment.
9589
*
96-
* Utilizing the {@see Timestamp::now()} method, this function captures the precise moment of invocation down to the
97-
* nanosecond. This ensures that the {@see DateTime] instance represents an accurate and precise point in time.
98-
*
99-
* The method optionally accepts a timezone. If omitted, the DateTime object is created using the default system
100-
* timezone, as determined by {@see Timezone::default()}. This flexibility allows for the creation of DateTime instances
101-
* that are relevant in various global contexts without the need for manual timezone conversion by the caller.
90+
* This static method returns a {@see DateTime} object set to the current date and time. If a specific timezone is
91+
* provided, the returned {@see DateTime} will be adjusted to reflect the date and time in that timezone.
10292
*
10393
* @pure
10494
*/
105-
public static function now(?Timezone $timezone = null): DateTime
95+
public static function now(Timezone $timezone): DateTime
10696
{
107-
return self::fromTimestamp(Timestamp::now(), $timezone ?? Timezone::default());
97+
return self::fromTimestamp(Timestamp::now(), $timezone);
10898
}
10999

110100
/**
@@ -115,9 +105,6 @@ public static function now(?Timezone $timezone = null): DateTime
115105
* method combines the current date context with a specific time, offering a convenient way to specify times such
116106
* as "today at 14:00" in code.
117107
*
118-
* If a timezone is provided, the {@see DateTime} object is adjusted to reflect the specified time in that timezone. If
119-
* the timezone parameter is omitted, the system's default timezone, as determined by {@see Timezone::default()}, is used.
120-
*
121108
* The time components (hours, minutes, seconds, nanoseconds) must be within their valid ranges. The method
122109
* enforces these constraints and throws an {@see Exception\InvalidArgumentException} if any component is out of bounds.
123110
*
@@ -131,19 +118,15 @@ public static function now(?Timezone $timezone = null): DateTime
131118
*
132119
* @pure
133120
*/
134-
public static function todayAt(int $hours, int $minutes, int $seconds = 0, int $nanoseconds = 0, ?Timezone $timezone = null): DateTime
121+
public static function todayAt(Timezone $timezone, int $hours, int $minutes, int $seconds = 0, int $nanoseconds = 0): DateTime
135122
{
136123
return self::now($timezone)->withTime($hours, $minutes, $seconds, $nanoseconds);
137124
}
138125

139126
/**
140127
* Creates a {@see DateTime} instance from individual date and time components.
141128
*
142-
* This method constructs a DateTime object based on the specified year, month, day, hour, minute, second, and nanosecond,
143-
* taking into account the specified or default timezone. It validates the date and time components to ensure they form
144-
* a valid date-time. If the components are invalid (e.g., 30th February), an {@see Exception\InvalidArgumentException} is thrown.
145-
*
146-
* In cases where the specified time occurs twice (such as during the end of daylight saving time), the earlier occurrence
129+
* Note: In cases where the specified time occurs twice (such as during the end of daylight saving time), the earlier occurrence
147130
* is returned. To obtain the later occurrence, you can adjust the returned instance using `->plusHours(1)`.
148131
*
149132
* @param Month|int<1, 12> $month
@@ -159,96 +142,113 @@ public static function todayAt(int $hours, int $minutes, int $seconds = 0, int $
159142
*/
160143
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
161144
{
162-
$timezone ??= Timezone::default();
145+
if ($month instanceof Month) {
146+
$month = $month->value;
147+
}
163148

164-
return Internal\zone_override($timezone, static function () use ($timezone, $year, $month, $day, $hours, $minutes, $seconds, $nanoseconds): DateTime {
165-
if ($month instanceof Month) {
166-
$month = $month->value;
167-
}
149+
/** @var IntlCalendar $calendar */
150+
$calendar = IntlCalendar::createInstance(
151+
$timezone === null ? null : Internal\to_intl_timezone($timezone),
152+
);
168153

169-
$timestamp = Timestamp::fromRaw(
170-
mktime($hours, $minutes, $seconds, $month, $day, $year),
171-
$nanoseconds,
172-
);
154+
$calendar->set($year, $month - 1, $day, $hours, $minutes, $seconds);
173155

174-
$ret = new DateTime(
175-
$timezone,
176-
$timestamp,
177-
$year,
178-
$month,
179-
$day,
180-
$hours,
181-
$minutes,
182-
$seconds,
183-
$nanoseconds,
156+
// Validate the date-time components by comparing them to what was set
157+
if (
158+
!($calendar->get(IntlCalendar::FIELD_YEAR) === $year &&
159+
($calendar->get(IntlCalendar::FIELD_MONTH) + 1) === $month &&
160+
$calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH) === $day &&
161+
$calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY) === $hours &&
162+
$calendar->get(IntlCalendar::FIELD_MINUTE) === $minutes &&
163+
$calendar->get(IntlCalendar::FIELD_SECOND) === $seconds)
164+
) {
165+
throw new Exception\InvalidArgumentException(
166+
'The given components do not form a valid date-time.',
184167
);
168+
}
169+
170+
$timestampInSeconds = (int) ($calendar->getTime() / MILLISECONDS_PER_SECOND);
171+
$timestamp = Timestamp::fromRaw($timestampInSeconds, $nanoseconds);
185172

186-
// mktime() doesn't throw on invalid date/time, but silently returns a
187-
// timestamp that doesn't match the input; so we check for that here.
188-
if ($ret->getParts() !== DateTime::fromTimestamp($timestamp, $timezone)->getParts()) {
189-
throw new Exception\InvalidArgumentException(Str\format(
190-
'The given components do not form a valid date-time in the timezone "%s"',
191-
$timezone->value,
192-
));
193-
}
194-
195-
return $ret;
196-
});
173+
return new self(
174+
$timezone,
175+
$timestamp,
176+
$year,
177+
$month,
178+
$day,
179+
$hours,
180+
$minutes,
181+
$seconds,
182+
$nanoseconds
183+
);
197184
}
198185

199186
/**
200187
* Creates a {@see DateTime} instance from a timestamp, representing the same point in time.
201188
*
202189
* This method converts a {@see Timestamp} into a {@see DateTime} instance calculated for the specified timezone.
203-
* It extracts and uses the date and time parts corresponding to the given timestamp in the provided or default timezone.
204-
*
205-
* @param Timezone|null $timezone The timezone for the DateTime instance. Defaults to the system's default timezone if null.
206190
*
207191
* @pure
208192
*/
209-
public static function fromTimestamp(Timestamp $timestamp, ?Timezone $timezone = null): DateTime
193+
public static function fromTimestamp(Timestamp $timestamp, Timezone $timezone): DateTime
210194
{
211-
$timezone ??= Timezone::default();
212-
213-
return Internal\zone_override($timezone, static function () use ($timezone, $timestamp): DateTime {
214-
[$s, $ns] = $timestamp->toRaw();
215-
$parts = getdate($s);
216-
217-
return new static(
218-
$timezone,
219-
$timestamp,
220-
$parts['year'],
221-
$parts['mon'],
222-
$parts['mday'],
223-
$parts['hours'],
224-
$parts['minutes'],
225-
$parts['seconds'],
226-
$ns,
227-
);
228-
});
195+
/** @var IntlCalendar $calendar */
196+
$calendar = IntlCalendar::createInstance(
197+
Internal\to_intl_timezone($timezone),
198+
);
199+
200+
$calendar->setTime(
201+
$timestamp->getSeconds() * MILLISECONDS_PER_SECOND,
202+
);
203+
204+
$year = $calendar->get(IntlCalendar::FIELD_YEAR);
205+
$month = $calendar->get(IntlCalendar::FIELD_MONTH) + 1;
206+
$day = $calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH);
207+
$hour = $calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY);
208+
$minute = $calendar->get(IntlCalendar::FIELD_MINUTE);
209+
$second = $calendar->get(IntlCalendar::FIELD_SECOND);
210+
$nanoseconds = $timestamp->getNanoseconds();
211+
212+
return new static(
213+
$timezone,
214+
$timestamp,
215+
$year,
216+
$month,
217+
$day,
218+
$hour,
219+
$minute,
220+
$second,
221+
$nanoseconds,
222+
);
229223
}
230224

231225
/**
232-
* Parses a date-time string and returns a {@see DateTime} instance for the specified or default timezone.
226+
* Creates a {@see DateTime} instance from a date/time string according to a specific pattern.
233227
*
234-
* This method interprets a date-time string, potentially relative (e.g., "next Thursday", "10 minutes ago") or absolute
235-
* (e.g., "2023-03-15 12:00:00"), into a {@see DateTime} object. An optional base time (`$relative_to`) can be provided to resolve
236-
* relative date-time expressions. If `$relative_to` is not provided, the current system time is used as the base.
228+
* This method allows parsing of date/time strings that conform to custom patterns,
229+
* making it versatile for handling various date/time formats.
237230
*
238-
* @param string $raw_string The date-time string to parse.
239-
* @param Timezone|null $timezone The timezone to use for the resulting DateTime instance. Defaults to the system's default timezone if null.
240-
* @param TemporalInterface|null $relative_to The temporal context used to interpret relative date-time expressions. Defaults to the current system time if null.
231+
* @throws Exception\RuntimeException If parsing fails or the date/time string is invalid.
232+
*/
233+
public static function fromPattern(string|DatePattern $pattern, string $raw_string, Timezone $timezone, ?Locale $locale = null): self
234+
{
235+
return self::fromTimestamp(
236+
Timestamp::fromPattern($pattern, $raw_string, $timezone, $locale),
237+
$timezone,
238+
);
239+
}
240+
241+
/**
242+
* Parses a date/time string into a {@see DateTime} instance.
241243
*
242-
* @throws Exception\InvalidArgumentException If parsing fails or the date-time string is invalid.
244+
* This method is a convenience wrapper for parsing date/time strings without specifying a custom pattern.
243245
*
244-
* @see https://www.php.net/manual/en/datetime.formats.php For information on supported date and time formats.
246+
* @throws Exception\RuntimeException If parsing fails or the format of the date/time string is invalid.
245247
*/
246-
public static function parse(string $raw_string, ?Timezone $timezone = null, ?TemporalInterface $relative_to = null): self
248+
public static function parse(string $raw_string, Timezone $timezone, ?Locale $locale = null): self
247249
{
248-
$timezone ??= Timezone::default();
249-
250250
return self::fromTimestamp(
251-
Timestamp::parse($raw_string, $timezone, $relative_to),
251+
Timestamp::parse($raw_string, $timezone, $locale),
252252
$timezone,
253253
);
254254
}

src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,10 @@
44

55
namespace Psl\DateTime;
66

7-
use DateTime as NativeDateTime;
8-
use DateTimeZone as NativeDateTimeZone;
9-
use IntlDateFormatter;
107
use Psl\DateTime;
118
use Psl\Locale;
129
use Psl\Math;
1310

14-
use function date;
15-
use function Psl\DateTime;
16-
1711
/**
1812
* @require-implements DateTimeInterface
1913
*/
@@ -421,9 +415,7 @@ public function getWeekday(): Weekday
421415
*/
422416
public function isDaylightSavingTime(): bool
423417
{
424-
return Internal\zone_override($this->getTimezone(), function (): bool {
425-
return date('I', $this->getTimestamp()->getSeconds()) === '1';
426-
});
418+
return !$this->getTimezone()->getDaylightSavingTimeOffset($this)->isZero();
427419
}
428420

429421
/**
@@ -618,25 +610,21 @@ public function minusDays(int $days): static
618610
}
619611

620612
/**
621-
* Formats the date and time according to the specified format and locale.
613+
* Formats the date and time of this instance into a string based on the provided pattern, timezone, and locale.
614+
*
615+
* If no pattern is specified, a default pattern will be used.
616+
*
617+
* The method uses the associated timezone of the instance by default, but this can be overridden with the
618+
* provided timezone parameter.
622619
*
623-
* @param DateFormat|string|null $format The format string or {@see DateFormat}. If null, the default format is used.
624-
* @param Locale\Locale|null $locale The locale for formatting. If null, the default locale is used.
620+
* The method also accounts for locale-specific formatting rules if a locale is provided.
625621
*
626622
* @mutation-free
623+
*
624+
* @note The default pattern is subject to change at any time and should not be relied upon for consistent formatting.
627625
*/
628-
public function format(DateFormat|string|null $format = null, ?Locale\Locale $locale = null): string
626+
public function format(DatePattern|string|null $pattern = null, ?Timezone $timezone = null, ?Locale\Locale $locale = null): string
629627
{
630-
return Internal\zone_override($this->getTimezone(), function () use ($locale, $format): string {
631-
$obj = new NativeDateTime();
632-
$obj->setTimestamp($this->getTimestamp()->getSeconds());
633-
$obj->setTimezone(new NativeDateTimeZone($this->getTimezone()->value));
634-
635-
if ($format instanceof DateFormat) {
636-
$format = $format->value;
637-
}
638-
639-
return IntlDateFormatter::formatObject($obj, $format, $locale?->value);
640-
});
628+
return $this->getTimestamp()->format($pattern, $timezone ?? $this->getTimezone(), $locale);
641629
}
642630
}

0 commit comments

Comments
 (0)