Instant::toISOString() and ZonedDateTime::toISOString() emitted a compact time that dropped zero seconds (e.g. 2023-11-14T22:14Z), which is valid ISO 8601 but not valid RFC 3339. Section 5.6 of RFC 3339 requires the full HH:MM:SS time even when the seconds are zero. These types are meant for interoperability, so they must always emit RFC 3339 compliant output. Build the time part of ZonedDateTime::toISOString() explicitly with a mandatory seconds component instead of delegating to LocalTime's compact formatter. Instant delegates to ZonedDateTime, so it is fixed transitively; jsonSerialize() and __toString() route through toISOString(). LocalTime's documented compact format is intentional and left unchanged. This changes the serialized string representation and is therefore a BC break; fixtures that pinned the compact output are updated accordingly.
782 lines
24 KiB
PHP
782 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Brick\DateTime;
|
|
|
|
use Brick\DateTime\Parser\DateTimeParseException;
|
|
use Brick\DateTime\Parser\DateTimeParser;
|
|
use Brick\DateTime\Parser\DateTimeParseResult;
|
|
use Brick\DateTime\Parser\IsoParsers;
|
|
use DateTime;
|
|
use DateTimeImmutable;
|
|
use DateTimeInterface;
|
|
use JsonSerializable;
|
|
use Override;
|
|
use Stringable;
|
|
|
|
use function assert;
|
|
use function intdiv;
|
|
|
|
/**
|
|
* A date-time with a time-zone in the ISO-8601 calendar system.
|
|
*
|
|
* A ZonedDateTime can be viewed as a LocalDateTime along with a time zone
|
|
* and targets a specific point in time.
|
|
*/
|
|
final readonly class ZonedDateTime implements JsonSerializable, Stringable
|
|
{
|
|
/**
|
|
* Private constructor. Use a factory method to obtain an instance.
|
|
*
|
|
* @param LocalDateTime $localDateTime The local date-time.
|
|
* @param TimeZoneOffset $timeZoneOffset The time-zone offset from UTC/Greenwich.
|
|
* @param TimeZone $timeZone The time-zone. It is either a TimeZoneRegion if this ZonedDateTime is
|
|
* region-based, or the same instance as the offset if this ZonedDateTime
|
|
* is offset-based.
|
|
* @param Instant $instant The instant represented by this ZonedDateTime.
|
|
*/
|
|
private function __construct(
|
|
private LocalDateTime $localDateTime,
|
|
private TimeZoneOffset $timeZoneOffset,
|
|
private TimeZone $timeZone,
|
|
private Instant $instant,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Creates a ZonedDateTime from a LocalDateTime and a TimeZone.
|
|
*
|
|
* This resolves the local date-time to an instant on the time-line.
|
|
*
|
|
* When a TimeZoneOffset is used, the local date-time can be converted to an instant without ambiguity.
|
|
*
|
|
* When a TimeZoneRegion is used, Daylight Saving Time can make the conversion more complex.
|
|
* There are 3 cases:
|
|
*
|
|
* - Normal: when there is only one valid offset for the date-time. The conversion is then as straightforward
|
|
* as when using a TimeZoneOffset. This is fortunately the case for the vast majority of the year.
|
|
* - Gap: when there is no valid offset for the date-time. This happens when the clock jumps forward
|
|
* typically due to a DST transition from "winter" to "summer". The date-times between the two times
|
|
* of the transition do not exist.
|
|
* - Overlap: when there are two valid offsets for the date-time. This happens when the clock is set back
|
|
* typically due to a DST transition from "summer" to "winter". The date-times between the two times
|
|
* of the transition can be resolved to two different offsets, representing two different instants
|
|
* on the time-line.
|
|
*
|
|
* The strategy for resolving gaps and overlaps is the following:
|
|
*
|
|
* - If the local date-time falls in the middle of a gap, then the resulting date-time will be shifted forward
|
|
* by the length of the gap, and the later offset, typically "summer" time, will be used.
|
|
* - If the local date-time falls in the middle of an overlap, then the offset closest to UTC will be used.
|
|
*/
|
|
public static function of(LocalDateTime $dateTime, TimeZone $timeZone): ZonedDateTime
|
|
{
|
|
$dtz = $timeZone->toNativeDateTimeZone();
|
|
$dt = new DateTime((string) $dateTime->withNano(0), $dtz);
|
|
|
|
$instant = Instant::of($dt->getTimestamp(), $dateTime->getNano());
|
|
|
|
if ($timeZone instanceof TimeZoneOffset) {
|
|
$timeZoneOffset = $timeZone;
|
|
} else {
|
|
$timeZoneOffset = TimeZoneOffset::ofTotalSeconds($dt->getOffset());
|
|
}
|
|
|
|
// The time can be affected if the date-time is not valid for the given time-zone due to a DST transition,
|
|
// so we have to re-compute the local date-time from the DateTime object.
|
|
// DateTime does not support nanos of seconds, so we just copy the nanos back from the original date-time.
|
|
$dateTime = LocalDateTime::parse($dt->format('Y-m-d\TH:i:s'))->withNano($dateTime->getNano());
|
|
|
|
return new ZonedDateTime($dateTime, $timeZoneOffset, $timeZone, $instant);
|
|
}
|
|
|
|
/**
|
|
* Creates a ZonedDateTime from an instant and a time zone.
|
|
*
|
|
* This resolves the instant to a date and time without ambiguity.
|
|
*/
|
|
public static function ofInstant(Instant $instant, TimeZone $timeZone): ZonedDateTime
|
|
{
|
|
$dateTimeZone = $timeZone->toNativeDateTimeZone();
|
|
|
|
// We need to pass a DateTimeZone to avoid a PHP warning...
|
|
$dateTime = new DateTime('@' . $instant->getEpochSecond(), $dateTimeZone);
|
|
|
|
// ... but this DateTimeZone is ignored because of the timestamp, so we set it again.
|
|
$dateTime->setTimezone($dateTimeZone);
|
|
|
|
$localDateTime = LocalDateTime::parse($dateTime->format('Y-m-d\TH:i:s'));
|
|
$localDateTime = $localDateTime->withNano($instant->getNano());
|
|
|
|
if ($timeZone instanceof TimeZoneOffset) {
|
|
$timeZoneOffset = $timeZone;
|
|
} else {
|
|
$timeZoneOffset = TimeZoneOffset::ofTotalSeconds($dateTime->getOffset());
|
|
}
|
|
|
|
return new ZonedDateTime($localDateTime, $timeZoneOffset, $timeZone, $instant);
|
|
}
|
|
|
|
/**
|
|
* Returns the current date-time in the given time-zone, according to the given clock.
|
|
*
|
|
* If no clock is provided, the system clock is used.
|
|
*/
|
|
public static function now(TimeZone $timeZone, ?Clock $clock = null): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::ofInstant(Instant::now($clock), $timeZone);
|
|
}
|
|
|
|
/**
|
|
* Obtains an instance of `ZonedDateTime` from a set of date-time fields.
|
|
*
|
|
* This method is only useful to parsers.
|
|
*
|
|
* @throws DateTimeException If the zoned date-time is not valid.
|
|
* @throws DateTimeParseException If required fields are missing from the result.
|
|
*/
|
|
public static function from(DateTimeParseResult $result): ZonedDateTime
|
|
{
|
|
$localDateTime = LocalDateTime::from($result);
|
|
|
|
$timeZoneOffset = TimeZoneOffset::from($result);
|
|
|
|
if ($result->hasField(Field\TimeZoneRegion::NAME)) {
|
|
$timeZone = TimeZoneRegion::from($result);
|
|
} else {
|
|
$timeZone = $timeZoneOffset;
|
|
}
|
|
|
|
return ZonedDateTime::of(
|
|
$localDateTime,
|
|
$timeZone,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Obtains an instance of `ZonedDateTime` from a text string.
|
|
*
|
|
* Valid examples:
|
|
* - `2007-12-03T10:15:30:45Z`
|
|
* - `2007-12-03T10:15:30+01:00`
|
|
* - `2007-12-03T10:15:30+01:00[Europe/Paris]`
|
|
*
|
|
* @param string $text The text to parse.
|
|
* @param DateTimeParser|null $parser The parser to use, defaults to the ISO 8601 parser.
|
|
*
|
|
* @throws DateTimeException If the date is not valid.
|
|
* @throws DateTimeParseException If the text string does not follow the expected format.
|
|
*/
|
|
public static function parse(string $text, ?DateTimeParser $parser = null): ZonedDateTime
|
|
{
|
|
if ($parser === null) {
|
|
$parser = IsoParsers::zonedDateTime();
|
|
}
|
|
|
|
return ZonedDateTime::from($parser->parse($text));
|
|
}
|
|
|
|
/**
|
|
* Creates a ZonedDateTime from a native DateTime or DateTimeImmutable object.
|
|
*
|
|
* @throws DateTimeException If the DateTime object has no timezone.
|
|
*/
|
|
public static function fromNativeDateTime(DateTimeInterface $dateTime): ZonedDateTime
|
|
{
|
|
$localDateTime = LocalDateTime::fromNativeDateTime($dateTime);
|
|
|
|
$dateTimeZone = $dateTime->getTimezone();
|
|
|
|
if ($dateTimeZone === false) { // @phpstan-ignore identical.alwaysFalse
|
|
// @codeCoverageIgnoreStart
|
|
throw new DateTimeException('This DateTime object has no timezone.');
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
$timeZone = TimeZone::fromNativeDateTimeZone($dateTimeZone);
|
|
|
|
if ($timeZone instanceof TimeZoneOffset) {
|
|
$timeZoneOffset = $timeZone;
|
|
} else {
|
|
$timeZoneOffset = TimeZoneOffset::ofTotalSeconds($dateTime->getOffset());
|
|
}
|
|
|
|
$instant = Instant::of($dateTime->getTimestamp(), $localDateTime->getNano());
|
|
|
|
return new ZonedDateTime($localDateTime, $timeZoneOffset, $timeZone, $instant);
|
|
}
|
|
|
|
/**
|
|
* Returns the `LocalDateTime` part of this `ZonedDateTime`.
|
|
*/
|
|
public function getDateTime(): LocalDateTime
|
|
{
|
|
return $this->localDateTime;
|
|
}
|
|
|
|
/**
|
|
* Returns the `LocalDate` part of this `ZonedDateTime`.
|
|
*/
|
|
public function getDate(): LocalDate
|
|
{
|
|
return $this->localDateTime->getDate();
|
|
}
|
|
|
|
/**
|
|
* Returns the `LocalTime` part of this `ZonedDateTime`.
|
|
*/
|
|
public function getTime(): LocalTime
|
|
{
|
|
return $this->localDateTime->getTime();
|
|
}
|
|
|
|
public function getYear(): int
|
|
{
|
|
return $this->localDateTime->getYear();
|
|
}
|
|
|
|
/**
|
|
* Returns the month-of-year as a Month enum.
|
|
*/
|
|
public function getMonth(): Month
|
|
{
|
|
return $this->localDateTime->getMonth();
|
|
}
|
|
|
|
/**
|
|
* Returns the month-of-year value from 1 to 12.
|
|
*
|
|
* @return int<1, 12>
|
|
*/
|
|
public function getMonthValue(): int
|
|
{
|
|
return $this->localDateTime->getMonthValue();
|
|
}
|
|
|
|
/**
|
|
* @return int<1, 31>
|
|
*/
|
|
public function getDayOfMonth(): int
|
|
{
|
|
return $this->localDateTime->getDayOfMonth();
|
|
}
|
|
|
|
public function getDayOfWeek(): DayOfWeek
|
|
{
|
|
return $this->localDateTime->getDayOfWeek();
|
|
}
|
|
|
|
/**
|
|
* @return int<1, 366>
|
|
*/
|
|
public function getDayOfYear(): int
|
|
{
|
|
return $this->localDateTime->getDayOfYear();
|
|
}
|
|
|
|
public function getHour(): int
|
|
{
|
|
return $this->localDateTime->getHour();
|
|
}
|
|
|
|
public function getMinute(): int
|
|
{
|
|
return $this->localDateTime->getMinute();
|
|
}
|
|
|
|
public function getSecond(): int
|
|
{
|
|
return $this->localDateTime->getSecond();
|
|
}
|
|
|
|
public function getEpochSecond(): int
|
|
{
|
|
return $this->instant->getEpochSecond();
|
|
}
|
|
|
|
public function getNano(): int
|
|
{
|
|
return $this->instant->getNano();
|
|
}
|
|
|
|
/**
|
|
* Returns the time-zone, region or offset.
|
|
*/
|
|
public function getTimeZone(): TimeZone
|
|
{
|
|
return $this->timeZone;
|
|
}
|
|
|
|
/**
|
|
* Returns the time-zone offset.
|
|
*/
|
|
public function getTimeZoneOffset(): TimeZoneOffset
|
|
{
|
|
return $this->timeZoneOffset;
|
|
}
|
|
|
|
public function getInstant(): Instant
|
|
{
|
|
return $this->instant;
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with a different date.
|
|
*/
|
|
public function withDate(LocalDate $date): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->withDate($date), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with a different time.
|
|
*/
|
|
public function withTime(LocalTime $time): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->withTime($time), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the year altered.
|
|
*/
|
|
public function withYear(int $year): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->withYear($year), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the month-of-year altered.
|
|
*/
|
|
public function withMonth(int|Month $month): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->withMonth($month), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the day-of-month altered.
|
|
*/
|
|
public function withDay(int $day): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->withDay($day), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the hour-of-day altered.
|
|
*/
|
|
public function withHour(int $hour): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->withHour($hour), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the minute-of-hour altered.
|
|
*/
|
|
public function withMinute(int $minute): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->withMinute($minute), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the second-of-minute altered.
|
|
*/
|
|
public function withSecond(int $second): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->withSecond($second), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the nano-of-second altered.
|
|
*/
|
|
public function withNano(int $nano): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->withNano($nano), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this `ZonedDateTime` with a different time-zone,
|
|
* retaining the local date-time if possible.
|
|
*/
|
|
public function withTimeZoneSameLocal(TimeZone $timeZone): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime, $timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this date-time with a different time-zone, retaining the instant.
|
|
*/
|
|
public function withTimeZoneSameInstant(TimeZone $timeZone): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::ofInstant($this->instant, $timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this date-time with the time-zone set to the offset.
|
|
*
|
|
* This returns a zoned date-time where the time-zone is the same as `getOffset()`.
|
|
* The local date-time, offset and instant of the result will be the same as in this date-time.
|
|
*
|
|
* Setting the date-time to a fixed single offset means that any future
|
|
* calculations, such as addition or subtraction, have no complex edge cases
|
|
* due to time-zone rules.
|
|
* This might also be useful when sending a zoned date-time across a network,
|
|
* as most protocols, such as ISO-8601, only handle offsets, and not region-based time zones.
|
|
*/
|
|
public function withFixedOffsetTimeZone(): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime, $this->timeZoneOffset);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified Period added.
|
|
*/
|
|
public function plusPeriod(Period $period): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->plusPeriod($period), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified Duration added.
|
|
*/
|
|
public function plusDuration(Duration $duration): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::ofInstant($this->instant->plus($duration), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns an Interval from this ZonedDateTime (inclusive) to the given one (exclusive).
|
|
*
|
|
* @throws DateTimeException If the given ZonedDateTime is before this ZonedDateTime.
|
|
*/
|
|
public function getIntervalTo(ZonedDateTime $that): Interval
|
|
{
|
|
return $this->getInstant()->getIntervalTo($that->getInstant());
|
|
}
|
|
|
|
/**
|
|
* Returns a Duration representing the time elapsed between this ZonedDateTime and the given one.
|
|
* This method will return a negative duration if the given ZonedDateTime is before the current one.
|
|
*/
|
|
public function getDurationTo(ZonedDateTime $that): Duration
|
|
{
|
|
return Duration::between($this->getInstant(), $that->getInstant());
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in years added.
|
|
*/
|
|
public function plusYears(int $years): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->plusYears($years), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in months added.
|
|
*/
|
|
public function plusMonths(int $months): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->plusMonths($months), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in weeks added.
|
|
*/
|
|
public function plusWeeks(int $weeks): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->plusWeeks($weeks), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in days added.
|
|
*/
|
|
public function plusDays(int $days): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::of($this->localDateTime->plusDays($days), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in hours added.
|
|
*/
|
|
public function plusHours(int $hours): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::ofInstant($this->instant->plusHours($hours), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in minutes added.
|
|
*/
|
|
public function plusMinutes(int $minutes): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::ofInstant($this->instant->plusMinutes($minutes), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in seconds added.
|
|
*/
|
|
public function plusSeconds(int $seconds): ZonedDateTime
|
|
{
|
|
return ZonedDateTime::ofInstant($this->instant->plusSeconds($seconds), $this->timeZone);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified Period subtracted.
|
|
*/
|
|
public function minusPeriod(Period $period): ZonedDateTime
|
|
{
|
|
return $this->plusPeriod($period->negated());
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified Duration subtracted.
|
|
*/
|
|
public function minusDuration(Duration $duration): ZonedDateTime
|
|
{
|
|
return $this->plusDuration($duration->negated());
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in years subtracted.
|
|
*/
|
|
public function minusYears(int $years): ZonedDateTime
|
|
{
|
|
return $this->plusYears(-$years);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in months subtracted.
|
|
*/
|
|
public function minusMonths(int $months): ZonedDateTime
|
|
{
|
|
return $this->plusMonths(-$months);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in weeks subtracted.
|
|
*/
|
|
public function minusWeeks(int $weeks): ZonedDateTime
|
|
{
|
|
return $this->plusWeeks(-$weeks);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in days subtracted.
|
|
*/
|
|
public function minusDays(int $days): ZonedDateTime
|
|
{
|
|
return $this->plusDays(-$days);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in hours subtracted.
|
|
*/
|
|
public function minusHours(int $hours): ZonedDateTime
|
|
{
|
|
return $this->plusHours(-$hours);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in minutes subtracted.
|
|
*/
|
|
public function minusMinutes(int $minutes): ZonedDateTime
|
|
{
|
|
return $this->plusMinutes(-$minutes);
|
|
}
|
|
|
|
/**
|
|
* Returns a copy of this ZonedDateTime with the specified period in seconds subtracted.
|
|
*/
|
|
public function minusSeconds(int $seconds): ZonedDateTime
|
|
{
|
|
return $this->plusSeconds(-$seconds);
|
|
}
|
|
|
|
/**
|
|
* Compares this ZonedDateTime with another.
|
|
*
|
|
* The comparison is performed on the instant.
|
|
*
|
|
* @return -1|0|1 If this zoned date-time is before, on, or after the given one.
|
|
*/
|
|
public function compareTo(ZonedDateTime $that): int
|
|
{
|
|
return $this->instant->compareTo($that->instant);
|
|
}
|
|
|
|
/**
|
|
* Returns whether this ZonedDateTime equals another.
|
|
*
|
|
* The comparison is performed on the instant.
|
|
*/
|
|
public function isEqualTo(ZonedDateTime $that): bool
|
|
{
|
|
return $this->compareTo($that) === 0;
|
|
}
|
|
|
|
/**
|
|
* Returns whether this ZonedDateTime is after another.
|
|
*
|
|
* The comparison is performed on the instant.
|
|
*/
|
|
public function isAfter(ZonedDateTime $that): bool
|
|
{
|
|
return $this->compareTo($that) === 1;
|
|
}
|
|
|
|
/**
|
|
* Returns whether this ZonedDateTime is after or equal to another.
|
|
*
|
|
* The comparison is performed on the instant.
|
|
*/
|
|
public function isAfterOrEqualTo(ZonedDateTime $that): bool
|
|
{
|
|
return $this->compareTo($that) >= 0;
|
|
}
|
|
|
|
/**
|
|
* Returns whether this ZonedDateTime is before another.
|
|
*
|
|
* The comparison is performed on the instant.
|
|
*/
|
|
public function isBefore(ZonedDateTime $that): bool
|
|
{
|
|
return $this->compareTo($that) === -1;
|
|
}
|
|
|
|
/**
|
|
* Returns whether this ZonedDateTime is before or equal to another.
|
|
*
|
|
* The comparison is performed on the instant.
|
|
*/
|
|
public function isBeforeOrEqualTo(ZonedDateTime $that): bool
|
|
{
|
|
return $this->compareTo($that) <= 0;
|
|
}
|
|
|
|
public function isBetweenInclusive(ZonedDateTime $from, ZonedDateTime $to): bool
|
|
{
|
|
return $this->isAfterOrEqualTo($from) && $this->isBeforeOrEqualTo($to);
|
|
}
|
|
|
|
public function isBetweenExclusive(ZonedDateTime $from, ZonedDateTime $to): bool
|
|
{
|
|
return $this->isAfter($from) && $this->isBefore($to);
|
|
}
|
|
|
|
/**
|
|
* Returns whether this ZonedDateTime is in the future, according to the given clock.
|
|
*
|
|
* If no clock is provided, the system clock is used.
|
|
*/
|
|
public function isFuture(?Clock $clock = null): bool
|
|
{
|
|
return $this->instant->isFuture($clock);
|
|
}
|
|
|
|
/**
|
|
* Returns whether this ZonedDateTime is in the past, according to the given clock.
|
|
*
|
|
* If no clock is provided, the system clock is used.
|
|
*/
|
|
public function isPast(?Clock $clock = null): bool
|
|
{
|
|
return $this->instant->isPast($clock);
|
|
}
|
|
|
|
/**
|
|
* Converts this ZonedDateTime to a native DateTime object.
|
|
*
|
|
* Note that the native DateTime object supports a precision up to the microsecond,
|
|
* so the nanoseconds are rounded down to the nearest microsecond.
|
|
*/
|
|
public function toNativeDateTime(): DateTime
|
|
{
|
|
$second = $this->localDateTime->getSecond();
|
|
|
|
// round down to the microsecond
|
|
$nano = $this->localDateTime->getNano();
|
|
$nano = 1000 * intdiv($nano, 1000);
|
|
|
|
$dateTime = (string) $this->localDateTime->withNano($nano);
|
|
$dateTimeZone = $this->timeZone->toNativeDateTimeZone();
|
|
|
|
$format = 'Y-m-d\TH:i';
|
|
|
|
if ($second !== 0 || $nano !== 0) {
|
|
$format .= ':s';
|
|
|
|
if ($nano !== 0) {
|
|
$format .= '.u';
|
|
}
|
|
}
|
|
|
|
$nativeDateTime = DateTime::createFromFormat($format, $dateTime, $dateTimeZone);
|
|
|
|
assert($nativeDateTime !== false);
|
|
|
|
return $nativeDateTime;
|
|
}
|
|
|
|
public function toNativeDateTimeImmutable(): DateTimeImmutable
|
|
{
|
|
return DateTimeImmutable::createFromMutable($this->toNativeDateTime());
|
|
}
|
|
|
|
/**
|
|
* Serializes as a string using {@see ZonedDateTime::toISOString()}.
|
|
*
|
|
* @return non-empty-string
|
|
*/
|
|
#[Override]
|
|
public function jsonSerialize(): string
|
|
{
|
|
return $this->toISOString();
|
|
}
|
|
|
|
/**
|
|
* Returns the ISO 8601 representation of this zoned date time.
|
|
*
|
|
* Unlike {@see LocalTime::toISOString()}, the time part always includes the
|
|
* seconds, so that the output is a valid RFC 3339 date-time. This type is
|
|
* primarily meant for interoperability, and RFC 3339 section 5.6 requires
|
|
* the full `HH:MM:SS` time even when the seconds are zero.
|
|
*
|
|
* @return non-empty-string
|
|
*/
|
|
public function toISOString(): string
|
|
{
|
|
$time = $this->localDateTime->getTime();
|
|
|
|
$hour = $time->getHour();
|
|
$minute = $time->getMinute();
|
|
$second = $time->getSecond();
|
|
$nano = $time->getNano();
|
|
|
|
$string = $this->localDateTime->getDate()
|
|
. 'T'
|
|
. ($hour < 10 ? '0' . $hour : $hour)
|
|
. ':'
|
|
. ($minute < 10 ? '0' . $minute : $minute)
|
|
. ':'
|
|
. ($second < 10 ? '0' . $second : $second)
|
|
. ($nano !== 0 ? '.' . rtrim(str_pad((string) $nano, 9, '0', STR_PAD_LEFT), '0') : '')
|
|
. $this->timeZoneOffset;
|
|
|
|
if ($this->timeZone instanceof TimeZoneRegion) {
|
|
$string .= '[' . $this->timeZone . ']';
|
|
}
|
|
|
|
return $string;
|
|
}
|
|
|
|
/**
|
|
* {@see ZonedDateTime::toISOString()}.
|
|
*
|
|
* @return non-empty-string
|
|
*/
|
|
#[Override]
|
|
public function __toString(): string
|
|
{
|
|
return $this->toISOString();
|
|
}
|
|
}
|