Upstream brick/date-time state of the touched files before 9dd1445216

This commit is contained in:
upstream committed 2026-09-04 08:53:18 +02:00
commit d37085b46a
5 files changed
+3249

No files matched your search

+377
View File
@@ -0,0 +1,377 @@
<?php
declare(strict_types=1);
namespace Brick\DateTime;
use JsonSerializable;
use Override;
use Stringable;
use function intdiv;
use function rtrim;
use function str_pad;
use const PHP_INT_MAX;
use const PHP_INT_MIN;
use const STR_PAD_LEFT;
/**
* Represents a point in time, with a nanosecond precision.
*
* Instant represents the computer view of the timeline. It unambiguously represents a point in time,
* without any calendar concept of date, time or time zone. It is not very meaningful to humans,
* but can be converted to a `ZonedDateTime` by providing a time zone.
*/
final readonly class Instant implements JsonSerializable, Stringable
{
/**
* Private constructor. Use of() to obtain an Instant.
*
* @param int $epochSecond The number of seconds since the epoch of 1970-01-01T00:00:00Z.
* @param int $nano The nanosecond adjustment to the epoch second, validated in the range 0 to 999,999,999.
*/
private function __construct(
private int $epochSecond,
private int $nano,
) {
}
/**
* Returns an Instant representing a number of seconds and an adjustment in nanoseconds.
*
* This method allows an arbitrary number of nanoseconds to be passed in.
* The factory will alter the values of the second and nanosecond in order
* to ensure that the stored nanosecond is in the range 0 to 999,999,999.
* For example, the following will result in exactly the same instant:
*
* * Instant::of(3, 1);
* * Instant::of(4, -999_999_999);
* * Instant::of(2, 1_000_000_001);
*
* @param int $epochSecond The number of seconds since the UNIX epoch of 1970-01-01T00:00:00Z.
* @param int $nanoAdjustment The adjustment to the epoch second in nanoseconds.
*/
public static function of(int $epochSecond, int $nanoAdjustment = 0): Instant
{
$nanos = $nanoAdjustment % LocalTime::NANOS_PER_SECOND;
$epochSecond += intdiv($nanoAdjustment - $nanos, LocalTime::NANOS_PER_SECOND);
if ($nanos < 0) {
$nanos += LocalTime::NANOS_PER_SECOND;
$epochSecond--;
}
return new Instant($epochSecond, $nanos);
}
public static function epoch(): Instant
{
/** @var Instant|null $epoch */
static $epoch = null;
return $epoch ??= new Instant(0, 0);
}
public static function now(?Clock $clock = null): Instant
{
if ($clock === null) {
$clock = DefaultClock::get();
}
return $clock->getTime();
}
/**
* Returns the minimum supported instant.
*
* This could be used by an application as a "far past" instant.
*/
public static function min(): Instant
{
/** @var Instant|null $min */
static $min = null;
return $min ??= new Instant(PHP_INT_MIN, 0);
}
/**
* Returns the maximum supported instant.
*
* This could be used by an application as a "far future" instant.
*/
public static function max(): Instant
{
/** @var Instant|null $max */
static $max = null;
return $max ??= new Instant(PHP_INT_MAX, 999_999_999);
}
public function plus(Duration $duration): Instant
{
if ($duration->isZero()) {
return $this;
}
$seconds = $this->epochSecond + $duration->getSeconds();
$nanos = $this->nano + $duration->getNanos();
return Instant::of($seconds, $nanos);
}
public function minus(Duration $duration): Instant
{
if ($duration->isZero()) {
return $this;
}
return $this->plus($duration->negated());
}
public function plusSeconds(int $seconds): Instant
{
if ($seconds === 0) {
return $this;
}
return new Instant($this->epochSecond + $seconds, $this->nano);
}
public function minusSeconds(int $seconds): Instant
{
return $this->plusSeconds(-$seconds);
}
public function plusMinutes(int $minutes): Instant
{
return $this->plusSeconds($minutes * LocalTime::SECONDS_PER_MINUTE);
}
public function minusMinutes(int $minutes): Instant
{
return $this->plusMinutes(-$minutes);
}
public function plusHours(int $hours): Instant
{
return $this->plusSeconds($hours * LocalTime::SECONDS_PER_HOUR);
}
public function minusHours(int $hours): Instant
{
return $this->plusHours(-$hours);
}
public function plusDays(int $days): Instant
{
return $this->plusSeconds($days * LocalTime::SECONDS_PER_DAY);
}
/**
* Returns a copy of this Instant with the epoch second altered.
*/
public function withEpochSecond(int $epochSecond): Instant
{
if ($epochSecond === $this->epochSecond) {
return $this;
}
return new Instant($epochSecond, $this->nano);
}
/**
* Returns a copy of this Instant with the nano-of-second altered.
*
* @throws DateTimeException If the nano-of-second if not valid.
*/
public function withNano(int $nano): Instant
{
if ($nano === $this->nano) {
return $this;
}
Field\NanoOfSecond::check($nano);
return new Instant($this->epochSecond, $nano);
}
public function minusDays(int $days): Instant
{
return $this->plusDays(-$days);
}
public function getEpochSecond(): int
{
return $this->epochSecond;
}
public function getNano(): int
{
return $this->nano;
}
/**
* Compares this instant with another.
*
* @return -1|0|1 If this instant is before, on, or after the given instant.
*/
public function compareTo(Instant $that): int
{
$seconds = $this->getEpochSecond() - $that->getEpochSecond();
if ($seconds !== 0) {
return $seconds > 0 ? 1 : -1;
}
$nanos = $this->getNano() - $that->getNano();
if ($nanos !== 0) {
return $nanos > 0 ? 1 : -1;
}
return 0;
}
/**
* Returns whether this instant equals another.
*/
public function isEqualTo(Instant $that): bool
{
return $this->compareTo($that) === 0;
}
/**
* Returns whether this instant is after another.
*/
public function isAfter(Instant $that): bool
{
return $this->compareTo($that) === 1;
}
/**
* Returns whether this instant is after or equal to another.
*/
public function isAfterOrEqualTo(Instant $that): bool
{
return $this->compareTo($that) >= 0;
}
/**
* Returns whether this instant is before another.
*/
public function isBefore(Instant $that): bool
{
return $this->compareTo($that) === -1;
}
/**
* Returns whether this instant is before or equal to another.
*/
public function isBeforeOrEqualTo(Instant $that): bool
{
return $this->compareTo($that) <= 0;
}
public function isBetweenInclusive(Instant $from, Instant $to): bool
{
return $this->isAfterOrEqualTo($from) && $this->isBeforeOrEqualTo($to);
}
public function isBetweenExclusive(Instant $from, Instant $to): bool
{
return $this->isAfter($from) && $this->isBefore($to);
}
/**
* Returns whether this instant 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->isAfter(Instant::now($clock));
}
/**
* Returns whether this instant 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->isBefore(Instant::now($clock));
}
/**
* Returns a ZonedDateTime formed from this instant and the specified time-zone.
*/
public function atTimeZone(TimeZone $timeZone): ZonedDateTime
{
return ZonedDateTime::ofInstant($this, $timeZone);
}
/**
* Returns an Interval from this Instant (inclusive) to the given one (exclusive).
*
* @throws DateTimeException If the given Instant is before this Instant.
*/
public function getIntervalTo(Instant $that): Interval
{
return Interval::of($this, $that);
}
/**
* Returns a decimal representation of the timestamp represented by this instant.
*
* The output does not have trailing decimal zeros.
*
* Examples: `123456789`, `123456789.5`, `123456789.000000001`.
*/
public function toDecimal(): string
{
$result = (string) $this->epochSecond;
if ($this->nano !== 0) {
$nano = (string) $this->nano;
$nano = str_pad($nano, 9, '0', STR_PAD_LEFT);
$nano = rtrim($nano, '0');
$result .= '.' . $nano;
}
return $result;
}
/**
* Serializes as a string using {@see Instant::toISOString()}.
*
* @return non-empty-string
*/
#[Override]
public function jsonSerialize(): string
{
return $this->toISOString();
}
/**
* Returns the ISO 8601 representation of this instant.
*
* @return non-empty-string
*/
public function toISOString(): string
{
return (string) ZonedDateTime::ofInstant($this, TimeZone::utc());
}
/**
* {@see Instant::toISOString()}.
*
* @return non-empty-string
*/
#[Override]
public function __toString(): string
{
return $this->toISOString();
}
}
+761
View File
@@ -0,0 +1,761 @@
<?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.
*
* @return non-empty-string
*/
public function toISOString(): string
{
$string = $this->localDateTime . $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();
}
}
+667
View File
@@ -0,0 +1,667 @@
<?php
declare(strict_types=1);
namespace Brick\DateTime\Tests;
use Brick\DateTime\Clock\FixedClock;
use Brick\DateTime\DateTimeException;
use Brick\DateTime\Duration;
use Brick\DateTime\Instant;
use Brick\DateTime\TimeZone;
use PHPUnit\Framework\Attributes\DataProvider;
use function json_encode;
use const JSON_THROW_ON_ERROR;
use const PHP_INT_MAX;
use const PHP_INT_MIN;
/**
* Unit tests for class Instant.
*/
class InstantTest extends AbstractTestCase
{
/**
* @param int $seconds The duration in seconds.
* @param int $nanoAdjustment The nanoseconds adjustment to the duration.
* @param int $expectedSeconds The expected adjusted duration seconds.
* @param int $expectedNanos The expected adjusted duration nanoseconds.
*/
#[DataProvider('providerOf')]
public function testOf(int $seconds, int $nanoAdjustment, int $expectedSeconds, int $expectedNanos): void
{
$duration = Instant::of($seconds, $nanoAdjustment);
self::assertSame($expectedSeconds, $duration->getEpochSecond());
self::assertSame($expectedNanos, $duration->getNano());
}
public static function providerOf(): array
{
return [
[3, 1, 3, 1],
[4, -999999999, 3, 1],
[2, 1000000001, 3, 1],
[-3, 1, -3, 1],
[-4, 1000000001, -3, 1],
[-2, -999999999, -3, 1],
[1, -1000000001, -1, 999999999],
[-1, -1000000001, -3, 999999999],
];
}
public function testEpoch(): void
{
$epoch = Instant::epoch();
self::assertInstantIs(0, 0, $epoch);
self::assertSame($epoch, Instant::epoch());
}
public function testNow(): void
{
$clock = new FixedClock(Instant::of(123456789, 987654321));
self::assertInstantIs(123456789, 987654321, Instant::now($clock));
}
public function testMin(): void
{
$min = Instant::min();
self::assertInstantIs(PHP_INT_MIN, 0, $min);
self::assertSame($min, Instant::min());
}
public function testMax(): void
{
$max = Instant::max();
self::assertInstantIs(PHP_INT_MAX, 999999999, $max);
self::assertSame($max, Instant::max());
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusSeconds The seconds of the duration to add.
* @param int $plusNanos The nanos of the duration to add.
* @param int $expectedSecond The expected second of the result.
* @param int $expectedNano The expected nano of the result.
*/
#[DataProvider('providerPlus')]
public function testPlus(int $second, int $nano, int $plusSeconds, int $plusNanos, int $expectedSecond, int $expectedNano): void
{
$result = Instant::of($second, $nano)->plus(Duration::ofSeconds($plusSeconds, $plusNanos));
self::assertInstantIs($expectedSecond, $expectedNano, $result);
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusSeconds The seconds of the duration to add.
* @param int $plusNanos The nanos of the duration to add.
* @param int $expectedSecond The expected second of the result.
* @param int $expectedNano The expected nano of the result.
*/
#[DataProvider('providerPlus')]
public function testMinus(int $second, int $nano, int $plusSeconds, int $plusNanos, int $expectedSecond, int $expectedNano): void
{
$result = Instant::of($second, $nano)->minus(Duration::ofSeconds(-$plusSeconds, -$plusNanos));
self::assertInstantIs($expectedSecond, $expectedNano, $result);
}
public static function providerPlus(): array
{
return [
[123456, 789, 0, 0, 123456, 789],
[123456, 789, 123, 456789, 123579, 457578],
[123456, 789, -123, -456789, 123332, 999544000],
[123456789, 999999999, 1, 1, 123456791, 0],
[123456789, 0, -1, -1, 123456787, 999999999],
];
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusSeconds The number of seconds to add.
* @param int $expectedSecond The expected second of the result.
*/
#[DataProvider('providerPlusSeconds')]
public function testPlusSeconds(int $second, int $nano, int $plusSeconds, int $expectedSecond): void
{
$result = Instant::of($second, $nano)->plusSeconds($plusSeconds);
self::assertInstantIs($expectedSecond, $nano, $result);
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusSeconds The number of seconds to add.
* @param int $expectedSecond The expected second of the result.
*/
#[DataProvider('providerPlusSeconds')]
public function testMinusSeconds(int $second, int $nano, int $plusSeconds, int $expectedSecond): void
{
$result = Instant::of($second, $nano)->minusSeconds(-$plusSeconds);
self::assertInstantIs($expectedSecond, $nano, $result);
}
public static function providerPlusSeconds(): array
{
return [
[123456, 789, 0, 123456, 789],
[123456, 789, 1000, 124456],
[123456, 789, -1000, 122456],
[123456, 123456789, 456, 123912],
[123456, 123456789, -456, 123000],
[123456, 987, 1000000, 1123456],
[123456, 987, -1000000, -876544],
];
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusMinutes The number of minutes to add.
* @param int $expectedSecond The expected second of the result.
*/
#[DataProvider('providerPlusMinutes')]
public function testPlusMinutes(int $second, int $nano, int $plusMinutes, int $expectedSecond): void
{
$result = Instant::of($second, $nano)->plusMinutes($plusMinutes);
self::assertInstantIs($expectedSecond, $nano, $result);
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusMinutes The number of minutes to add.
* @param int $expectedSecond The expected second of the result.
*/
#[DataProvider('providerPlusMinutes')]
public function testMinusMinutes(int $second, int $nano, int $plusMinutes, int $expectedSecond): void
{
$result = Instant::of($second, $nano)->minusMinutes(-$plusMinutes);
self::assertInstantIs($expectedSecond, $nano, $result);
}
public static function providerPlusMinutes(): array
{
return [
[123456, 789, 0, 123456, 789],
[123456, 789, 1000, 183456],
[123456, 789, -1000, 63456],
[123456, 123456789, 456, 150816],
[123456, 123456789, -456, 96096],
[123456, 987, 1000000, 60123456],
[123456, 987, -1000000, -59876544],
];
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusHours The number of hours to add.
* @param int $expectedSecond The expected second of the result.
*/
#[DataProvider('providerPlusHours')]
public function testPlusHours(int $second, int $nano, int $plusHours, int $expectedSecond): void
{
$result = Instant::of($second, $nano)->plusHours($plusHours);
self::assertInstantIs($expectedSecond, $nano, $result);
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusHours The number of hours to add.
* @param int $expectedSecond The expected second of the result.
*/
#[DataProvider('providerPlusHours')]
public function testMinusHours(int $second, int $nano, int $plusHours, int $expectedSecond): void
{
$result = Instant::of($second, $nano)->minusHours(-$plusHours);
self::assertInstantIs($expectedSecond, $nano, $result);
}
public static function providerPlusHours(): array
{
return [
[123456, 789, 0, 123456, 789],
[123456, 789, 1000, 3723456],
[123456, 789, -1000, -3476544],
[123456, 123456789, 456, 1765056],
[123456, 123456789, -456, -1518144],
[123456, 987, 1, 127056],
[123456, 987, -1, 119856],
];
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusDays The number of days to add.
* @param int $expectedSecond The expected second of the result.
*/
#[DataProvider('providerPlusDays')]
public function testPlusDays(int $second, int $nano, int $plusDays, int $expectedSecond): void
{
$result = Instant::of($second, $nano)->plusDays($plusDays);
self::assertInstantIs($expectedSecond, $nano, $result);
}
/**
* @param int $second The base second.
* @param int $nano The base nano-of-second.
* @param int $plusDays The number of days to add.
* @param int $expectedSecond The expected second of the result.
*/
#[DataProvider('providerPlusDays')]
public function testMinusDays(int $second, int $nano, int $plusDays, int $expectedSecond): void
{
$result = Instant::of($second, $nano)->minusDays(-$plusDays);
self::assertInstantIs($expectedSecond, $nano, $result);
}
public static function providerPlusDays(): array
{
return [
[123456, 789, 0, 123456, 789],
[123456, 789, 1000, 86523456],
[123456, 789, -1000, -86276544],
[123456, 123456789, 456, 39521856],
[123456, 123456789, -456, -39274944],
[123456, 987, 1, 209856],
[123456, 987, -1, 37056],
];
}
public function testWithEpochSecond(): void
{
$instant = Instant::of(1234567890, 987654321);
self::assertInstantIs(2345678901, 987654321, $instant->withEpochSecond(2345678901));
}
public function testWithNano(): void
{
$instant = Instant::of(1234567890, 987654321);
self::assertInstantIs(1234567890, 123456789, $instant->withNano(123456789));
}
#[DataProvider('providerWithInvalidNanoThrowsException')]
public function testWithInvalidNanoThrowsException(int $nano): void
{
$instant = Instant::of(1234567890, 987654321);
$this->expectException(DateTimeException::class);
$instant->withNano($nano);
}
public static function providerWithInvalidNanoThrowsException(): array
{
return [
[-1],
[1000000000],
];
}
/**
* @param int $s1 The epoch second of the 1st instant.
* @param int $n1 The nanosecond adjustment of the 1st instant.
* @param int $s2 The epoch second of the 2nd instant.
* @param int $n2 The nanosecond adjustment of the 2nd instant.
* @param int $cmp The expected comparison value.
*/
#[DataProvider('providerCompareTo')]
public function testCompareTo(int $s1, int $n1, int $s2, int $n2, int $cmp): void
{
self::assertSame($cmp, Instant::of($s1, $n1)->compareTo(Instant::of($s2, $n2)));
}
/**
* @param int $s1 The epoch second of the 1st instant.
* @param int $n1 The nanosecond adjustment of the 1st instant.
* @param int $s2 The epoch second of the 2nd instant.
* @param int $n2 The nanosecond adjustment of the 2nd instant.
* @param int $cmp The comparison value.
*/
#[DataProvider('providerCompareTo')]
public function testIsEqualTo(int $s1, int $n1, int $s2, int $n2, int $cmp): void
{
self::assertSame($cmp === 0, Instant::of($s1, $n1)->isEqualTo(Instant::of($s2, $n2)));
}
/**
* @param int $s1 The epoch second of the 1st instant.
* @param int $n1 The nanosecond adjustment of the 1st instant.
* @param int $s2 The epoch second of the 2nd instant.
* @param int $n2 The nanosecond adjustment of the 2nd instant.
* @param int $cmp The comparison value.
*/
#[DataProvider('providerCompareTo')]
public function testIsAfter(int $s1, int $n1, int $s2, int $n2, int $cmp): void
{
self::assertSame($cmp === 1, Instant::of($s1, $n1)->isAfter(Instant::of($s2, $n2)));
}
/**
* @param int $s1 The epoch second of the 1st instant.
* @param int $n1 The nanosecond adjustment of the 1st instant.
* @param int $s2 The epoch second of the 2nd instant.
* @param int $n2 The nanosecond adjustment of the 2nd instant.
* @param int $cmp The comparison value.
*/
#[DataProvider('providerCompareTo')]
public function testIsAfterOrEqualTo(int $s1, int $n1, int $s2, int $n2, int $cmp): void
{
self::assertSame($cmp >= 0, Instant::of($s1, $n1)->isAfterOrEqualTo(Instant::of($s2, $n2)));
}
/**
* @param int $s1 The epoch second of the 1st instant.
* @param int $n1 The nanosecond adjustment of the 1st instant.
* @param int $s2 The epoch second of the 2nd instant.
* @param int $n2 The nanosecond adjustment of the 2nd instant.
* @param int $cmp The comparison value.
*/
#[DataProvider('providerCompareTo')]
public function testIsBefore(int $s1, int $n1, int $s2, int $n2, int $cmp): void
{
self::assertSame($cmp === -1, Instant::of($s1, $n1)->isBefore(Instant::of($s2, $n2)));
}
/**
* @param int $s1 The epoch second of the 1st instant.
* @param int $n1 The nanosecond adjustment of the 1st instant.
* @param int $s2 The epoch second of the 2nd instant.
* @param int $n2 The nanosecond adjustment of the 2nd instant.
* @param int $cmp The comparison value.
*/
#[DataProvider('providerCompareTo')]
public function testIsBeforeOrEqualTo(int $s1, int $n1, int $s2, int $n2, int $cmp): void
{
self::assertSame($cmp <= 0, Instant::of($s1, $n1)->isBeforeOrEqualTo(Instant::of($s2, $n2)));
}
/**
* @param int $testSecond The second of the test instant.
* @param int $testNano The nanosecond adjustment to the test instant.
* @param int $nowSecond The second of the current time.
* @param int $nowNano The nanosecond adjustment to the current time.
* @param int $cmp The comparison value.
*/
#[DataProvider('providerCompareTo')]
public function testIsFuture(int $testSecond, int $testNano, int $nowSecond, int $nowNano, int $cmp): void
{
$clock = new FixedClock(Instant::of($nowSecond, $nowNano));
self::assertSame($cmp === 1, Instant::of($testSecond, $testNano)->isFuture($clock));
}
/**
* @param int $testSecond The second of the test instant.
* @param int $testNano The nanosecond adjustment to the test instant.
* @param int $nowSecond The second of the current time.
* @param int $nowNano The nanosecond adjustment to the current time.
* @param int $cmp The comparison value.
*/
#[DataProvider('providerCompareTo')]
public function testIsPast(int $testSecond, int $testNano, int $nowSecond, int $nowNano, int $cmp): void
{
$clock = new FixedClock(Instant::of($nowSecond, $nowNano));
self::assertSame($cmp === -1, Instant::of($testSecond, $testNano)->isPast($clock));
}
public static function providerCompareTo(): array
{
return [
[-1, -1, -1, -1, 0],
[-1, -1, -1, 0, -1],
[-1, -1, -1, 1, -1],
[-1, -1, 0, -1, -1],
[-1, -1, 0, 0, -1],
[-1, -1, 0, 1, -1],
[-1, -1, 1, -1, -1],
[-1, -1, 1, 0, -1],
[-1, -1, 1, 1, -1],
[-1, 0, -1, -1, 1],
[-1, 0, -1, 0, 0],
[-1, 0, -1, 1, -1],
[-1, 0, 0, -1, -1],
[-1, 0, 0, 0, -1],
[-1, 0, 0, 1, -1],
[-1, 0, 1, -1, -1],
[-1, 0, 1, 0, -1],
[-1, 0, 1, 1, -1],
[-1, 1, -1, -1, 1],
[-1, 1, -1, 0, 1],
[-1, 1, -1, 1, 0],
[-1, 1, 0, -1, -1],
[-1, 1, 0, 0, -1],
[-1, 1, 0, 1, -1],
[-1, 1, 1, -1, -1],
[-1, 1, 1, 0, -1],
[-1, 1, 1, 1, -1],
[0, -1, -1, -1, 1],
[0, -1, -1, 0, 1],
[0, -1, -1, 1, 1],
[0, -1, 0, -1, 0],
[0, -1, 0, 0, -1],
[0, -1, 0, 1, -1],
[0, -1, 1, -1, -1],
[0, -1, 1, 0, -1],
[0, -1, 1, 1, -1],
[0, 0, -1, -1, 1],
[0, 0, -1, 0, 1],
[0, 0, -1, 1, 1],
[0, 0, 0, -1, 1],
[0, 0, 0, 0, 0],
[0, 0, 0, 1, -1],
[0, 0, 1, -1, -1],
[0, 0, 1, 0, -1],
[0, 0, 1, 1, -1],
[0, 1, -1, -1, 1],
[0, 1, -1, 0, 1],
[0, 1, -1, 1, 1],
[0, 1, 0, -1, 1],
[0, 1, 0, 0, 1],
[0, 1, 0, 1, 0],
[0, 1, 1, -1, -1],
[0, 1, 1, 0, -1],
[0, 1, 1, 1, -1],
[1, -1, -1, -1, 1],
[1, -1, -1, 0, 1],
[1, -1, -1, 1, 1],
[1, -1, 0, -1, 1],
[1, -1, 0, 0, 1],
[1, -1, 0, 1, 1],
[1, -1, 1, -1, 0],
[1, -1, 1, 0, -1],
[1, -1, 1, 1, -1],
[1, 0, -1, -1, 1],
[1, 0, -1, 0, 1],
[1, 0, -1, 1, 1],
[1, 0, 0, -1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 1, 1],
[1, 0, 1, -1, 1],
[1, 0, 1, 0, 0],
[1, 0, 1, 1, -1],
[1, 1, -1, -1, 1],
[1, 1, -1, 0, 1],
[1, 1, -1, 1, 1],
[1, 1, 0, -1, 1],
[1, 1, 0, 0, 1],
[1, 1, 0, 1, 1],
[1, 1, 1, -1, 1],
[1, 1, 1, 0, 1],
[1, 1, 1, 1, 0],
];
}
/**
* @param int $seconds The seconds value.
* @param int $nanos The nano seconds value.
* @param bool $isBetween Check the secs and nanos are between.
*/
#[DataProvider('providerIsBetweenInclusive')]
public function testIsBetweenInclusive(int $seconds, int $nanos, bool $isBetween): void
{
self::assertSame($isBetween, Instant::of($seconds, $nanos)->isBetweenInclusive(
Instant::of(-1, -1),
Instant::of(1, 1),
));
}
/**
* @param int $seconds The seconds value.
* @param int $nanos The nano seconds value.
* @param bool $isBetween Check the secs and nanos are between.
*/
#[DataProvider('providerIsBetweenExclusive')]
public function testIsBetweenExclusive(int $seconds, int $nanos, bool $isBetween): void
{
self::assertSame($isBetween, Instant::of($seconds, $nanos)->isBetweenExclusive(
Instant::of(-1, -1),
Instant::of(1, 1),
));
}
public static function providerIsBetweenExclusive(): array
{
return [
[-1, -2, false],
[-1, -1, false],
[-1, 0, true],
[-1, 1, true],
[0, -1, true],
[0, 0, true],
[0, 1, true],
[1, -1, true],
[1, 0, true],
[1, 1, false],
[1, 2, false],
];
}
public static function providerIsBetweenInclusive(): array
{
return [
[-1, -2, false],
[-1, -1, true],
[-1, 0, true],
[-1, 1, true],
[0, -1, true],
[0, 0, true],
[0, 1, true],
[1, -1, true],
[1, 0, true],
[1, 1, true],
[1, 2, false],
];
}
#[DataProvider('providerGetIntervalTo')]
public function testGetIntervalTo(int $second1, int $nano1, int $second2, int $nano2, string $expectedInterval): void
{
$actualResult = Instant::of($second1, $nano1)->getIntervalTo(Instant::of($second2, $nano2));
self::assertSame($expectedInterval, (string) $actualResult);
}
public static function providerGetIntervalTo(): array
{
return [
[1672567200, 0, 1672567200, 0, '2023-01-01T10:00Z/2023-01-01T10:00Z'],
[1672567200, 0, 1672567210, 0, '2023-01-01T10:00Z/2023-01-01T10:00:10Z'],
[1672567200, 1000000, 1672567210, 2000000, '2023-01-01T10:00:00.001Z/2023-01-01T10:00:10.002Z'],
[1672567200, 1, 1672567200, 9, '2023-01-01T10:00:00.000000001Z/2023-01-01T10:00:00.000000009Z'],
[1672567200, 0, 1672653600, 0, '2023-01-01T10:00Z/2023-01-02T10:00Z'],
];
}
/**
* @param int $second The epoch second.
* @param int $nano The nano adjustment.
* @param string $expected The expected decimal output.
*/
#[DataProvider('providerToDecimal')]
public function testToDecimal(int $second, int $nano, string $expected): void
{
self::assertSame($expected, Instant::of($second, $nano)->toDecimal());
}
public static function providerToDecimal(): array
{
return [
[123456789, 0, '123456789'],
[123456789, 1, '123456789.000000001'],
[123456789, 10, '123456789.00000001'],
[123456789, 100, '123456789.0000001'],
[123456789, 1000, '123456789.000001'],
[123456789, 10000, '123456789.00001'],
[123456789, 100000, '123456789.0001'],
[123456789, 1000000, '123456789.001'],
[123456789, 10000000, '123456789.01'],
[123456789, 100000000, '123456789.1'],
[123456789, 550000000, '123456789.55'],
];
}
/**
* @param int $epochSecond The epoch second to test.
* @param int $nano The nano adjustment to the epoch second.
* @param string $expectedString The expected string output.
*/
#[DataProvider('providerToString')]
public function testJsonSerialize(int $epochSecond, int $nano, string $expectedString): void
{
self::assertSame(json_encode($expectedString, JSON_THROW_ON_ERROR), json_encode(Instant::of($epochSecond, $nano), JSON_THROW_ON_ERROR));
}
/**
* @param int $epochSecond The epoch second to test.
* @param int $nano The nano adjustment to the epoch second.
* @param string $expectedString The expected string output.
*/
#[DataProvider('providerToString')]
public function testToISOString(int $epochSecond, int $nano, string $expectedString): void
{
self::assertSame($expectedString, Instant::of($epochSecond, $nano)->toISOString());
}
/**
* @param int $epochSecond The epoch second to test.
* @param int $nano The nano adjustment to the epoch second.
* @param string $expectedString The expected string output.
*/
#[DataProvider('providerToString')]
public function testToString(int $epochSecond, int $nano, string $expectedString): void
{
self::assertSame($expectedString, (string) Instant::of($epochSecond, $nano));
}
public static function providerToString(): array
{
return [
[-2000000000, 0, '1906-08-16T20:26:40Z'],
[-1, 0, '1969-12-31T23:59:59Z'],
[-1, 123, '1969-12-31T23:59:59.000000123Z'],
[0, 0, '1970-01-01T00:00Z'],
[0, 123456, '1970-01-01T00:00:00.000123456Z'],
[1, 0, '1970-01-01T00:00:01Z'],
[1, 123456789, '1970-01-01T00:00:01.123456789Z'],
[2000000000, 0, '2033-05-18T03:33:20Z'],
];
}
public function testAtTimeZone(): void
{
$timeZone = TimeZone::utc();
$instant = Instant::of(1000000000);
$result = $instant->atTimeZone($timeZone);
self::assertSame(1000000000, $result->getInstant()->getEpochSecond());
self::assertSame('2001-09-09T01:46:40', (string) $result->getDateTime());
}
}
+303
View File
@@ -0,0 +1,303 @@
<?php
declare(strict_types=1);
namespace Brick\DateTime\Tests;
use Brick\DateTime\DateTimeException;
use Brick\DateTime\Instant;
use Brick\DateTime\Interval;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Depends;
use function json_encode;
use const JSON_THROW_ON_ERROR;
/**
* Unit tests for class Interval.
*/
class IntervalTest extends AbstractTestCase
{
public function testEndInstantIsNotBeforeStartInstant(): void
{
$start = Instant::of(2000000000, 987654321);
$end = Instant::of(2000000009, 123456789);
$this->expectException(DateTimeException::class);
$this->expectExceptionMessage('The end instant must not be before the start instant.');
Interval::of($end, $start);
}
public function testGetStartEnd(): void
{
$start = Instant::of(2000000000, 987654321);
$end = Instant::of(2000000009, 123456789);
$interval = Interval::of($start, $end);
self::assertInstantIs(2000000000, 987654321, $interval->getStart());
self::assertInstantIs(2000000009, 123456789, $interval->getEnd());
}
#[Depends('testGetStartEnd')]
public function testWithStart(): void
{
$interval = Interval::of(
Instant::of(2000000000),
Instant::of(2000000001),
);
$newInterval = $interval->withStart(Instant::of(1999999999, 999999999));
self::assertNotSame($newInterval, $interval);
// ensure that the original isn't changed
self::assertInstantIs(2000000000, 0, $interval->getStart());
self::assertInstantIs(2000000001, 0, $interval->getEnd());
// test the new instance
self::assertInstantIs(1999999999, 999999999, $newInterval->getStart());
self::assertInstantIs(2000000001, 0, $newInterval->getEnd());
}
#[Depends('testGetStartEnd')]
public function testWithEnd(): void
{
$interval = Interval::of(
Instant::of(2000000000),
Instant::of(2000000001),
);
$newInterval = $interval->withEnd(Instant::of(2000000002, 222222222));
self::assertNotSame($newInterval, $interval);
// ensure that the original isn't changed
self::assertInstantIs(2000000000, 0, $interval->getStart());
self::assertInstantIs(2000000001, 0, $interval->getEnd());
// test the new instance
self::assertInstantIs(2000000000, 0, $newInterval->getStart());
self::assertInstantIs(2000000002, 222222222, $newInterval->getEnd());
}
public function testGetDuration(): void
{
$interval = Interval::of(
Instant::of(1999999999, 555555),
Instant::of(2000000001, 111),
);
$duration = $interval->getDuration();
self::assertDurationIs(1, 999444556, $duration);
}
#[DataProvider('providerContains')]
public function testContains(int $start, int $end, int $now, bool $expected, string $errorMessage): void
{
$interval = Interval::of(Instant::of($start), Instant::of($end));
self::assertSame($expected, $interval->contains(Instant::of($now)), $errorMessage);
}
public static function providerContains(): array
{
return [
'at the start' => [
1000000000,
2000000000,
1000000000,
true,
'an Interval must contain its start',
],
'at the end' => [
1000000000,
2000000000,
2000000000,
false,
'an Interval must not contain its end',
],
'in the middle' => [
1000000001,
1000000003,
1000000002,
true,
'an Interval must contain its intermediate values',
],
];
}
#[DataProvider('providerIntersectsWith')]
public function testIntersectsWith(int $start1, int $end1, int $start2, int $end2, bool $expected): void
{
$interval1 = Interval::of(Instant::of($start1), Instant::of($end1));
$interval2 = Interval::of(Instant::of($start2), Instant::of($end2));
self::assertSame($expected, $interval1->intersectsWith($interval2));
}
public static function providerIntersectsWith(): array
{
return [
'second is after first' => [
100000, 200000,
400000, 500000,
false,
],
'second is before first' => [
400000, 500000,
100000, 200000,
false,
],
'end of the first is start of the second' => [
100000, 200000,
200000, 300000,
false,
],
'start of the first is end of the second' => [
200000, 300000,
100000, 200000,
false,
],
'intersection' => [
100000, 200000,
150000, 250000,
true,
],
];
}
#[DataProvider('providerGetIntersectionWith')]
public function testGetIntersectionWith(
int $start1,
int $end1,
int $start2,
int $end2,
int $expectedStart,
int $expectedEnd,
): void {
$interval1 = Interval::of(Instant::of($start1), Instant::of($end1));
$interval2 = Interval::of(Instant::of($start2), Instant::of($end2));
$expected = Interval::of(Instant::of($expectedStart), Instant::of($expectedEnd));
self::assertTrue($expected->isEqualTo($interval1->getIntersectionWith($interval2)));
}
public static function providerGetIntersectionWith(): array
{
return [
'first before second' => [
100000, 200000,
150000, 250000,
150000, 200000,
],
'first after second' => [
150000, 250000,
100000, 200000,
150000, 200000,
],
'first inside second' => [
200000, 300000,
100000, 400000,
200000, 300000,
],
'second inside first' => [
100000, 400000,
200000, 300000,
200000, 300000,
],
'first = second' => [
5000, 6000,
5000, 6000,
5000, 6000,
],
];
}
public function testGetIntersectionWithInvalidParams(): void
{
$interval1 = Interval::of(Instant::of(100000), Instant::of(200000));
$interval2 = Interval::of(Instant::of(300000), Instant::of(400000));
$this->expectException(DateTimeException::class);
$this->expectExceptionMessage('Intervals "1970-01-02T03:46:40Z/1970-01-03T07:33:20Z" and "1970-01-04T11:20Z/1970-01-05T15:06:40Z" do not intersect.');
$interval1->getIntersectionWith($interval2);
}
#[DataProvider('providerIsEqualTo')]
public function testIsEqualTo(Interval $a, Interval $b, bool $expectedResult): void
{
self::assertSame($expectedResult, $a->isEqualTo($b));
self::assertSame($expectedResult, $b->isEqualTo($a));
}
public static function providerIsEqualTo(): array
{
return [
'start is not equal' => [
Interval::of(Instant::of(100000), Instant::of(200000)),
Interval::of(Instant::of(150000), Instant::of(200000)),
false,
],
'end is not equal' => [
Interval::of(Instant::of(100000), Instant::of(200000)),
Interval::of(Instant::of(100000), Instant::of(250000)),
false,
],
'both start and end are not equal' => [
Interval::of(Instant::of(100000), Instant::of(200000)),
Interval::of(Instant::of(150000), Instant::of(250000)),
false,
],
'intervals are equal' => [
Interval::of(Instant::of(100000), Instant::of(200000)),
Interval::of(Instant::of(100000), Instant::of(200000)),
true,
],
];
}
#[DataProvider('providerToString')]
public function testJsonSerialize(int $epochSecondStart, int $epochSecondEnd, string $expectedString): void
{
$interval = Interval::of(
Instant::of($epochSecondStart),
Instant::of($epochSecondEnd),
);
self::assertSame(json_encode($expectedString, JSON_THROW_ON_ERROR), json_encode($interval, JSON_THROW_ON_ERROR));
}
#[DataProvider('providerToString')]
public function testToISOString(int $epochSecondStart, int $epochSecondEnd, string $expectedString): void
{
$interval = Interval::of(
Instant::of($epochSecondStart),
Instant::of($epochSecondEnd),
);
self::assertSame($expectedString, $interval->toISOString());
}
#[DataProvider('providerToString')]
public function testToString(int $epochSecondStart, int $epochSecondEnd, string $expectedString): void
{
$interval = Interval::of(
Instant::of($epochSecondStart),
Instant::of($epochSecondEnd),
);
self::assertSame($expectedString, (string) $interval);
}
public static function providerToString(): array
{
return [
[1000000000, 1000000000, '2001-09-09T01:46:40Z/2001-09-09T01:46:40Z'],
[1000000000, 2000000000, '2001-09-09T01:46:40Z/2033-05-18T03:33:20Z'],
];
}
}
File diff suppressed because it is too large. Load diff