$month * @property-read int<1, 31> $day * @property-read int<1, 7> $dayOfWeek 1 (for Monday) through 7 (for Sunday) * @property-read int<0, 365> $dayOfYear 0 through 365 * @property-read int<1, 5> $weekOfMonth 1 through 5 * @property-read int<1, 53> $weekOfYear ISO-8601 week number of year, weeks starting on Monday * @property-read int<1, 31> $daysInMonth number of days in the given month * @property-read int $age does a diffInYears() with default parameters * @property-read int<1, 4> $quarter the quarter of this instance, 1 - 4 * @property-read int<1, 2> $half the half of the year, with 1 for months Jan...Jun and 2 for Jul...Dec. * @psalm-immutable * @psalm-consistent-constructor */ class ChronosDate implements Stringable { use FormattingTrait; /** * Default format to use for __toString method when type juggling occurs. * * @var string */ public const DEFAULT_TO_STRING_FORMAT = 'Y-m-d'; /** * Format to use for __toString method when type juggling occurs. * * @var string */ protected static string $toStringFormat = self::DEFAULT_TO_STRING_FORMAT; /** * Names of days of the week. * * @var array */ protected static array $days = [ Chronos::MONDAY => 'Monday', Chronos::TUESDAY => 'Tuesday', Chronos::WEDNESDAY => 'Wednesday', Chronos::THURSDAY => 'Thursday', Chronos::FRIDAY => 'Friday', Chronos::SATURDAY => 'Saturday', Chronos::SUNDAY => 'Sunday', ]; /** * Instance of the diff formatting object. * * @var \Cake\Chronos\DifferenceFormatterInterface|null */ protected static ?DifferenceFormatterInterface $diffFormatter = null; /** * Errors from last time createFromFormat() was called. * * @var array|false */ protected static array|false $lastErrors = false; /** * @var \DateTimeImmutable */ protected DateTimeImmutable $native; /** * Create a new Immutable Date instance. * * Dates do not have time or timezone components exposed. Internally * ChronosDate wraps a PHP DateTimeImmutable but limits modifications * to only those that operate on day values. * * By default dates will be calculated from the server's default timezone. * You can use the `timezone` parameter to use a different timezone. Timezones * are used when parsing relative date expressions like `today` and `yesterday` * but do not participate in parsing values like `2022-01-01`. * * @param \Cake\Chronos\ChronosDate|\DateTimeInterface|string $time Fixed or relative time * @param \DateTimeZone|string|null $timezone The time zone used for 'now' */ public function __construct( ChronosDate|DateTimeInterface|string $time = 'now', DateTimeZone|string|null $timezone = null ) { $this->native = $this->createNative($time, $timezone); } /** * Initializes the PHP DateTimeImmutable object. * * @param \Cake\Chronos\ChronosDate|\DateTimeInterface|string $time Fixed or relative time * @param \DateTimeZone|string|null $timezone The time zone used for 'now' * @return \DateTimeImmutable */ protected function createNative( ChronosDate|DateTimeInterface|string $time, DateTimeZone|string|null $timezone ): DateTimeImmutable { if (!is_string($time)) { return new DateTimeImmutable($time->format('Y-m-d 00:00:00')); } $timezone ??= date_default_timezone_get(); $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); $testNow = Chronos::getTestNow(); if ($testNow === null) { $time = new DateTimeImmutable($time, $timezone); return new DateTimeImmutable($time->format('Y-m-d 00:00:00')); } $testNow = $testNow->setTimezone($timezone); if ($time !== 'now') { $testNow = $testNow->modify($time); } return new DateTimeImmutable($testNow->format('Y-m-d 00:00:00')); } /** * Get today's date. * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return static */ public static function now(DateTimeZone|string|null $timezone = null): static { return new static('now', $timezone); } /** * Get today's date. * * @param \DateTimeZone|string|null $timezone Time zone to use for today. * @return static */ public static function today(DateTimeZone|string|null $timezone = null): static { return static::now($timezone); } /** * Get tomorrow's date. * * @param \DateTimeZone|string|null $timezone Time zone to use for tomorrow. * @return static */ public static function tomorrow(DateTimeZone|string|null $timezone = null): static { return new static('tomorrow', $timezone); } /** * Get yesterday's date. * * @param \DateTimeZone|string|null $timezone Time zone to use for yesterday. * @return static */ public static function yesterday(DateTimeZone|string|null $timezone = null): static { return new static('yesterday', $timezone); } /** * Create an instance from a string. This is an alias for the * constructor that allows better fluent syntax as it allows you to do * Chronos::parse('Monday next week')->fn() rather than * (new Chronos('Monday next week'))->fn() * * @param \Cake\Chronos\ChronosDate|\DateTimeInterface|string $time The strtotime compatible string to parse * @return static */ public static function parse(ChronosDate|DateTimeInterface|string $time): static { return new static($time); } /** * Create an instance from a specific date. * * @param int $year The year to create an instance with. * @param int $month The month to create an instance with. * @param int $day The day to create an instance with. * @return static */ public static function create(int $year, int $month, int $day): static { $instance = static::createFromFormat( 'Y-m-d', sprintf('%s-%s-%s', 0, $month, $day), ); return $instance->addYears($year); } /** * Create an instance from a specific format * * @param string $format The date() compatible format string. * @param string $time The formatted date string to interpret. * @return static * @throws \InvalidArgumentException */ public static function createFromFormat( string $format, string $time, ): static { $dateTime = DateTimeImmutable::createFromFormat($format, $time); static::$lastErrors = DateTimeImmutable::getLastErrors(); if (!$dateTime) { $message = static::$lastErrors ? implode(PHP_EOL, static::$lastErrors['errors']) : 'Unknown error'; throw new InvalidArgumentException($message); } return new static($dateTime); } /** * Returns parse warnings and errors from the last ``createFromFormat()`` * call. * * Returns the same data as DateTimeImmutable::getLastErrors(). * * @return array|false */ public static function getLastErrors(): array|false { return static::$lastErrors; } /** * Creates an instance from an array of date values. * * Allowed values: * - year * - month * - day * * @param array $values Array of date and time values. * @return static */ public static function createFromArray(array $values): static { $formatted = sprintf('%04d-%02d-%02d ', $values['year'], $values['month'], $values['day']); return static::parse($formatted); } /** * Get the difference formatter instance or overwrite the current one. * * @param \Cake\Chronos\DifferenceFormatterInterface|null $formatter The formatter instance when setting. * @return \Cake\Chronos\DifferenceFormatterInterface The formatter instance. */ public static function diffFormatter(?DifferenceFormatterInterface $formatter = null): DifferenceFormatterInterface { if ($formatter === null) { if (static::$diffFormatter === null) { static::$diffFormatter = new DifferenceFormatter(); } return static::$diffFormatter; } return static::$diffFormatter = $formatter; } /** * Add an Interval to a Date * * Any changes to the time will be ignored and reset to 00:00:00 * * @param \DateInterval $interval The interval to modify this date by. * @return static A modified Date instance */ public function add(DateInterval $interval): static { if ($interval->f > 0 || $interval->s > 0 || $interval->i > 0 || $interval->h > 0) { throw new InvalidArgumentException('Cannot add intervals with time components'); } $new = clone $this; $new->native = $new->native->add($interval)->setTime(0, 0, 0); return $new; } /** * Subtract an Interval from a Date. * * Any changes to the time will be ignored and reset to 00:00:00 * * @param \DateInterval $interval The interval to modify this date by. * @return static A modified Date instance */ public function sub(DateInterval $interval): static { if ($interval->f > 0 || $interval->s > 0 || $interval->i > 0 || $interval->h > 0) { throw new InvalidArgumentException('Cannot subtract intervals with time components'); } $new = clone $this; $new->native = $new->native->sub($interval)->setTime(0, 0, 0); return $new; } /** * Creates a new instance with date modified according to DateTimeImmutable::modifier(). * * Attempting to change a time component will raise an exception * * @param string $modifier Date modifier * @return static */ public function modify(string $modifier): static { if (preg_match('/hour|minute|second/', $modifier)) { throw new InvalidArgumentException('Cannot modify date objects by time values'); } $new = clone $this; $new->native = $new->native->modify($modifier); if ($new->native === false) { throw new InvalidArgumentException(sprintf('Unable to modify date using `%s`', $modifier)); } if ($new->format('H:i:s') !== '00:00:00') { $new->native = $new->native->setTime(0, 0, 0); } return $new; } /** * Sets the date. * * @param int $year The year to set. * @param int $month The month to set. * @param int $day The day to set. * @return static */ public function setDate(int $year, int $month, int $day): static { $new = clone $this; $new->native = $new->native->setDate($year, $month, $day); return $new; } /** * Sets the date according to the ISO 8601 standard * * @param int $year Year of the date. * @param int $week Week of the date. * @param int $dayOfWeek Offset from the first day of the week. * @return static */ public function setISODate(int $year, int $week, int $dayOfWeek = 1): static { $new = clone $this; $new->native = $new->native->setISODate($year, $week, $dayOfWeek); return $new; } /** * Returns the difference between this instance and target. * * @param \Cake\Chronos\ChronosDate $target Target instance * @param bool $absolute Whether the interval is forced to be positive * @return \DateInterval */ public function diff(ChronosDate $target, bool $absolute = false): DateInterval { return $this->native->diff($target->native, $absolute); } /** * Returns formatted date string according to DateTimeImmutable::format(). * * @param string $format String format * @return string */ public function format(string $format): string { return $this->native->format($format); } /** * Set the instance's year * * @param int $value The year value. * @return static */ public function year(int $value): static { return $this->setDate($value, $this->month, $this->day); } /** * Set the instance's month * * @param int $value The month value. * @return static */ public function month(int $value): static { return $this->setDate($this->year, $value, $this->day); } /** * Set the instance's day * * @param int $value The day value. * @return static */ public function day(int $value): static { return $this->setDate($this->year, $this->month, $value); } /** * Add years to the instance. Positive $value travel forward while * negative $value travel into the past. * * If the new ChronosDate does not exist, the last day of the month is used * instead instead of overflowing into the next month. * * ### Example: * * ``` * (new Chronos('2015-01-03'))->addYears(1); // Results in 2016-01-03 * * (new Chronos('2012-02-29'))->addYears(1); // Results in 2013-02-28 * ``` * * @param int $value The number of years to add. * @return static */ public function addYears(int $value): static { $month = $this->month; $date = $this->modify($value . ' years'); if ($date->month !== $month) { return $date->modify('last day of previous month'); } return $date; } /** * Remove years from the instance. * * Has the same behavior as `addYears()`. * * @param int $value The number of years to remove. * @return static */ public function subYears(int $value): static { return $this->addYears(-$value); } /** * Add years with overflowing to the instance. Positive $value * travels forward while negative $value travels into the past. * * If the new ChronosDate does not exist, the days overflow into the next month. * * ### Example: * * ``` * (new Chronos('2012-02-29'))->addYearsWithOverflow(1); // Results in 2013-03-01 * ``` * * @param int $value The number of years to add. * @return static */ public function addYearsWithOverflow(int $value): static { return $this->modify($value . ' year'); } /** * Remove years with overflow from the instance * * Has the same behavior as `addYeasrWithOverflow()`. * * @param int $value The number of years to remove. * @return static */ public function subYearsWithOverflow(int $value): static { return $this->addYearsWithOverflow(-1 * $value); } /** * Add months to the instance. Positive $value travels forward while * negative $value travels into the past. * * When adding or subtracting months, if the resulting time is a date * that does not exist, the result of this operation will always be the * last day of the intended month. * * ### Example: * * ``` * (new Chronos('2015-01-03'))->addMonths(1); // Results in 2015-02-03 * * (new Chronos('2015-01-31'))->addMonths(1); // Results in 2015-02-28 * ``` * * @param int $value The number of months to add. * @return static */ public function addMonths(int $value): static { $day = $this->day; $date = $this->modify($value . ' months'); if ($date->day !== $day) { return $date->modify('last day of previous month'); } return $date; } /** * Remove months from the instance * * Has the same behavior as `addMonths()`. * * @param int $value The number of months to remove. * @return static */ public function subMonths(int $value): static { return $this->addMonths(-$value); } /** * Add months with overflowing to the instance. Positive $value * travels forward while negative $value travels into the past. * * If the new ChronosDate does not exist, the days overflow into the next month. * * ### Example: * * ``` * (new Chronos('2012-01-30'))->addMonthsWithOverflow(1); // Results in 2013-03-01 * ``` * * @param int $value The number of months to add. * @return static */ public function addMonthsWithOverflow(int $value): static { return $this->modify($value . ' months'); } /** * Add months with overflowing to the instance. Positive $value * travels forward while negative $value travels into the past. * * If the new ChronosDate does not exist, the days overflow into the next month. * * ### Example: * * ``` * (new Chronos('2012-01-30'))->addMonthsWithOverflow(1); // Results in 2013-03-01 * ``` * * @param int $value The number of months to remove. * @return static */ public function subMonthsWithOverflow(int $value): static { return $this->addMonthsWithOverflow(-1 * $value); } /** * Add days to the instance. Positive $value travels forward while * negative $value travels into the past. * * @param int $value The number of days to add. * @return static */ public function addDays(int $value): static { return $this->modify("$value days"); } /** * Remove days from the instance * * @param int $value The number of days to remove. * @return static */ public function subDays(int $value): static { return $this->addDays(-$value); } /** * Add weekdays to the instance. Positive $value travels forward while * negative $value travels into the past. * * @param int $value The number of weekdays to add. * @return static */ public function addWeekdays(int $value): static { return $this->modify($value . ' weekdays, ' . $this->format('H:i:s')); } /** * Remove weekdays from the instance * * @param int $value The number of weekdays to remove. * @return static */ public function subWeekdays(int $value): static { return $this->addWeekdays(-$value); } /** * Add weeks to the instance. Positive $value travels forward while * negative $value travels into the past. * * @param int $value The number of weeks to add. * @return static */ public function addWeeks(int $value): static { return $this->modify("$value week"); } /** * Remove weeks to the instance * * @param int $value The number of weeks to remove. * @return static */ public function subWeeks(int $value): static { return $this->addWeeks(-$value); } /** * Resets the date to the first day of the month * * @return static */ public function startOfMonth(): static { return $this->modify('first day of this month'); } /** * Resets the date to end of the month * * @return static */ public function endOfMonth(): static { return $this->modify('last day of this month'); } /** * Resets the date to the first day of the year * * @return static */ public function startOfYear(): static { return $this->modify('first day of january'); } /** * Resets the date to end of the year * * @return static */ public function endOfYear(): static { return $this->modify('last day of december'); } /** * Resets the date to the first day of the decade * * @return static */ public function startOfDecade(): static { $year = $this->year - $this->year % Chronos::YEARS_PER_DECADE; return $this->modify("first day of january $year"); } /** * Resets the date to end of the decade * * @return static */ public function endOfDecade(): static { $year = $this->year - $this->year % Chronos::YEARS_PER_DECADE + Chronos::YEARS_PER_DECADE - 1; return $this->modify("last day of december $year"); } /** * Resets the date to the first day of the century * * @return static */ public function startOfCentury(): static { $year = $this->startOfYear() ->year($this->year - 1 - ($this->year - 1) % Chronos::YEARS_PER_CENTURY + 1) ->year; return $this->modify("first day of january $year"); } /** * Resets the date to end of the century and time to 23:59:59 * * @return static */ public function endOfCentury(): static { $y = $this->year - 1 - ($this->year - 1) % Chronos::YEARS_PER_CENTURY + Chronos::YEARS_PER_CENTURY; $year = $this->endOfYear() ->year($y) ->year; return $this->modify("last day of december $year"); } /** * Resets the date to the first day of week (defined in $weekStartsAt) * * @return static */ public function startOfWeek(): static { $dateTime = $this; if ($dateTime->dayOfWeek !== Chronos::getWeekStartsAt()) { $dateTime = $dateTime->previous(Chronos::getWeekStartsAt()); } return $dateTime; } /** * Resets the date to end of week (defined in $weekEndsAt) and time to 23:59:59 * * @return static */ public function endOfWeek(): static { $dateTime = $this; if ($dateTime->dayOfWeek !== Chronos::getWeekEndsAt()) { $dateTime = $dateTime->next(Chronos::getWeekEndsAt()); } return $dateTime; } /** * Modify to the next occurrence of a given day of the week. * If no dayOfWeek is provided, modify to the next occurrence * of the current day of the week. Use the supplied consts * to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. * @return static */ public function next(?int $dayOfWeek = null): static { if ($dayOfWeek === null) { $dayOfWeek = $this->dayOfWeek; } $day = static::$days[$dayOfWeek]; return $this->modify("next $day"); } /** * Modify to the previous occurrence of a given day of the week. * If no dayOfWeek is provided, modify to the previous occurrence * of the current day of the week. Use the supplied consts * to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. * @return static */ public function previous(?int $dayOfWeek = null): static { if ($dayOfWeek === null) { $dayOfWeek = $this->dayOfWeek; } $day = static::$days[$dayOfWeek]; return $this->modify("last $day"); } /** * Modify to the first occurrence of a given day of the week * in the current month. If no dayOfWeek is provided, modify to the * first day of the current month. Use the supplied consts * to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. * @return static */ public function firstOfMonth(?int $dayOfWeek = null): static { $day = $dayOfWeek === null ? 'day' : static::$days[$dayOfWeek]; return $this->modify("first $day of this month"); } /** * Modify to the last occurrence of a given day of the week * in the current month. If no dayOfWeek is provided, modify to the * last day of the current month. Use the supplied consts * to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. * @return static */ public function lastOfMonth(?int $dayOfWeek = null): static { $day = $dayOfWeek === null ? 'day' : static::$days[$dayOfWeek]; return $this->modify("last $day of this month"); } /** * Modify to the given occurrence of a given day of the week * in the current month. If the calculated occurrence is outside the scope * of the current month, then return false and no modifications are made. * Use the supplied consts to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int $nth The offset to use. * @param int $dayOfWeek The day of the week to move to. * @return static|false */ public function nthOfMonth(int $nth, int $dayOfWeek): static|false { $dateTime = $this->firstOfMonth(); $check = $dateTime->format('Y-m'); $dateTime = $dateTime->modify("+$nth " . static::$days[$dayOfWeek]); return $dateTime->format('Y-m') === $check ? $dateTime : false; } /** * Modify to the first occurrence of a given day of the week * in the current quarter. If no dayOfWeek is provided, modify to the * first day of the current quarter. Use the supplied consts * to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. * @return static */ public function firstOfQuarter(?int $dayOfWeek = null): static { return $this ->day(1) ->month($this->quarter * Chronos::MONTHS_PER_QUARTER - 2) ->firstOfMonth($dayOfWeek); } /** * Modify to the last occurrence of a given day of the week * in the current quarter. If no dayOfWeek is provided, modify to the * last day of the current quarter. Use the supplied consts * to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. * @return static */ public function lastOfQuarter(?int $dayOfWeek = null): static { return $this ->day(1) ->month($this->quarter * Chronos::MONTHS_PER_QUARTER) ->lastOfMonth($dayOfWeek); } /** * Modify to the given occurrence of a given day of the week * in the current quarter. If the calculated occurrence is outside the scope * of the current quarter, then return false and no modifications are made. * Use the supplied consts to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int $nth The offset to use. * @param int $dayOfWeek The day of the week to move to. * @return static|false */ public function nthOfQuarter(int $nth, int $dayOfWeek): static|false { $dateTime = $this->day(1)->month($this->quarter * Chronos::MONTHS_PER_QUARTER); $lastMonth = $dateTime->month; $year = $dateTime->year; $dateTime = $dateTime->firstOfQuarter()->modify("+$nth" . static::$days[$dayOfWeek]); return $lastMonth < $dateTime->month || $year !== $dateTime->year ? false : $dateTime; } /** * Modify to the first occurrence of a given day of the week * in the current year. If no dayOfWeek is provided, modify to the * first day of the current year. Use the supplied consts * to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. * @return static */ public function firstOfYear(?int $dayOfWeek = null): static { $day = $dayOfWeek === null ? 'day' : static::$days[$dayOfWeek]; return $this->modify("first $day of january"); } /** * Modify to the last occurrence of a given day of the week * in the current year. If no dayOfWeek is provided, modify to the * last day of the current year. Use the supplied consts * to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. * @return static */ public function lastOfYear(?int $dayOfWeek = null): static { $day = $dayOfWeek === null ? 'day' : static::$days[$dayOfWeek]; return $this->modify("last $day of december"); } /** * Modify to the given occurrence of a given day of the week * in the current year. If the calculated occurrence is outside the scope * of the current year, then return false and no modifications are made. * Use the supplied consts to indicate the desired dayOfWeek, ex. Chronos::MONDAY. * * @param int $nth The offset to use. * @param int $dayOfWeek The day of the week to move to. * @return static|false */ public function nthOfYear(int $nth, int $dayOfWeek): static|false { $dateTime = $this->firstOfYear()->modify("+$nth " . static::$days[$dayOfWeek]); return $this->year === $dateTime->year ? $dateTime : false; } /** * Determines if the instance is equal to another * * @param \Cake\Chronos\ChronosDate $other The instance to compare with. * @return bool */ public function equals(ChronosDate $other): bool { return $this->native == $other->native; } /** * Determines if the instance is not equal to another * * @param \Cake\Chronos\ChronosDate $other The instance to compare with. * @return bool */ public function notEquals(ChronosDate $other): bool { return !$this->equals($other); } /** * Determines if the instance is greater (after) than another * * @param \Cake\Chronos\ChronosDate $other The instance to compare with. * @return bool */ public function greaterThan(ChronosDate $other): bool { return $this->native > $other->native; } /** * Determines if the instance is greater (after) than or equal to another * * @param \Cake\Chronos\ChronosDate $other The instance to compare with. * @return bool */ public function greaterThanOrEquals(ChronosDate $other): bool { return $this->native >= $other->native; } /** * Determines if the instance is less (before) than another * * @param \Cake\Chronos\ChronosDate $other The instance to compare with. * @return bool */ public function lessThan(ChronosDate $other): bool { return $this->native < $other->native; } /** * Determines if the instance is less (before) or equal to another * * @param \Cake\Chronos\ChronosDate $other The instance to compare with. * @return bool */ public function lessThanOrEquals(ChronosDate $other): bool { return $this->native <= $other->native; } /** * Determines if the instance is between two others * * @param \Cake\Chronos\ChronosDate $start Start of target range * @param \Cake\Chronos\ChronosDate $end End of target range * @param bool $equals Whether to include the beginning and end of range * @return bool */ public function between(ChronosDate $start, ChronosDate $end, bool $equals = true): bool { if ($start->greaterThan($end)) { [$start, $end] = [$end, $start]; } if ($equals) { return $this->greaterThanOrEquals($start) && $this->lessThanOrEquals($end); } return $this->greaterThan($start) && $this->lessThan($end); } /** * Get the closest date from the instance. * * @param \Cake\Chronos\ChronosDate $first The instance to compare with. * @param \Cake\Chronos\ChronosDate $second The instance to compare with. * @param \Cake\Chronos\ChronosDate ...$others Others instance to compare with. * @return self */ public function closest(ChronosDate $first, ChronosDate $second, ChronosDate ...$others): ChronosDate { $closest = $first; $closestDiffInDays = $this->diffInDays($first); foreach ([$second, ...$others] as $other) { $otherDiffInDays = $this->diffInDays($other); if ($otherDiffInDays < $closestDiffInDays) { $closest = $other; $closestDiffInDays = $otherDiffInDays; } } return $closest; } /** * Get the farthest date from the instance. * * @param \Cake\Chronos\ChronosDate $first The instance to compare with. * @param \Cake\Chronos\ChronosDate $second The instance to compare with. * @param \Cake\Chronos\ChronosDate ...$others Others instance to compare with. * @return self */ public function farthest(ChronosDate $first, ChronosDate $second, ChronosDate ...$others): ChronosDate { $farthest = $first; $farthestDiffInDays = $this->diffInDays($first); foreach ([$second, ...$others] as $other) { $otherDiffInDays = $this->diffInDays($other); if ($otherDiffInDays > $farthestDiffInDays) { $farthest = $other; $farthestDiffInDays = $otherDiffInDays; } } return $farthest; } /** * Determines if the instance is a weekday * * @return bool */ public function isWeekday(): bool { return !$this->isWeekend(); } /** * Determines if the instance is a weekend day * * @return bool */ public function isWeekend(): bool { return in_array($this->dayOfWeek, Chronos::getWeekendDays(), true); } /** * Determines if the instance is yesterday * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isYesterday(DateTimeZone|string|null $timezone = null): bool { return $this->equals(static::yesterday($timezone)); } /** * Determines if the instance is today * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isToday(DateTimeZone|string|null $timezone = null): bool { return $this->equals(static::now($timezone)); } /** * Determines if the instance is tomorrow * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isTomorrow(DateTimeZone|string|null $timezone = null): bool { return $this->equals(static::tomorrow($timezone)); } /** * Determines if the instance is within the next week * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isNextWeek(DateTimeZone|string|null $timezone = null): bool { return $this->format('W o') === static::now($timezone)->addWeeks(1)->format('W o'); } /** * Determines if the instance is within the last week * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isLastWeek(DateTimeZone|string|null $timezone = null): bool { return $this->format('W o') === static::now($timezone)->subWeeks(1)->format('W o'); } /** * Determines if the instance is within the next month * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isNextMonth(DateTimeZone|string|null $timezone = null): bool { return $this->format('m Y') === static::now($timezone)->addMonths(1)->format('m Y'); } /** * Determines if the instance is within the last month * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isLastMonth(DateTimeZone|string|null $timezone = null): bool { return $this->format('m Y') === static::now($timezone)->subMonths(1)->format('m Y'); } /** * Determines if the instance is within the next year * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isNextYear(DateTimeZone|string|null $timezone = null): bool { return $this->year === static::now($timezone)->addYears(1)->year; } /** * Determines if the instance is within the last year * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isLastYear(DateTimeZone|string|null $timezone = null): bool { return $this->year === static::now($timezone)->subYears(1)->year; } /** * Determines if the instance is within the first half of year * * @return bool */ public function isFirstHalf(): bool { return $this->half === 1; } /** * Determines if the instance is within the second half of year * * @return bool */ public function isSecondHalf(): bool { return $this->half === 2; } /** * Determines if the instance is in the future, ie. greater (after) than now * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isFuture(DateTimeZone|string|null $timezone = null): bool { return $this->greaterThan(static::now($timezone)); } /** * Determines if the instance is in the past, ie. less (before) than now * * @param \DateTimeZone|string|null $timezone Time zone to use for now. * @return bool */ public function isPast(DateTimeZone|string|null $timezone = null): bool { return $this->lessThan(static::now($timezone)); } /** * Determines if the instance is a leap year * * @return bool */ public function isLeapYear(): bool { return $this->format('L') === '1'; } /** * Checks if this day is a Sunday. * * @return bool */ public function isSunday(): bool { return $this->dayOfWeek === Chronos::SUNDAY; } /** * Checks if this day is a Monday. * * @return bool */ public function isMonday(): bool { return $this->dayOfWeek === Chronos::MONDAY; } /** * Checks if this day is a Tuesday. * * @return bool */ public function isTuesday(): bool { return $this->dayOfWeek === Chronos::TUESDAY; } /** * Checks if this day is a Wednesday. * * @return bool */ public function isWednesday(): bool { return $this->dayOfWeek === Chronos::WEDNESDAY; } /** * Checks if this day is a Thursday. * * @return bool */ public function isThursday(): bool { return $this->dayOfWeek === Chronos::THURSDAY; } /** * Checks if this day is a Friday. * * @return bool */ public function isFriday(): bool { return $this->dayOfWeek === Chronos::FRIDAY; } /** * Checks if this day is a Saturday. * * @return bool */ public function isSaturday(): bool { return $this->dayOfWeek === Chronos::SATURDAY; } /** * Returns true this instance happened within the specified interval * * @param string|int $timeInterval the numeric value with space then time type. * Example of valid types: 6 hours, 2 days, 1 minute. * @return bool */ public function wasWithinLast(string|int $timeInterval): bool { $now = new static(new Chronos()); $interval = $now->modify('-' . $timeInterval); $thisTime = $this->format('U'); return $thisTime >= $interval->format('U') && $thisTime <= $now->format('U'); } /** * Returns true this instance will happen within the specified interval * * @param string|int $timeInterval the numeric value with space then time type. * Example of valid types: 6 hours, 2 days, 1 minute. * @return bool */ public function isWithinNext(string|int $timeInterval): bool { $now = new static(new Chronos()); $interval = $now->modify('+' . $timeInterval); $thisTime = $this->format('U'); return $thisTime <= $interval->format('U') && $thisTime >= $now->format('U'); } /** * Get the difference by the given interval using a filter callable * * @param \DateInterval $interval An interval to traverse by * @param callable $callback The callback to use for filtering. * @param \Cake\Chronos\ChronosDate|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @param int $options DatePeriod options, {@see https://www.php.net/manual/en/class.dateperiod.php} * @return int */ public function diffFiltered( DateInterval $interval, callable $callback, ?ChronosDate $other = null, bool $absolute = true, int $options = 0 ): int { $start = $this; $end = $other ?? new ChronosDate(Chronos::now()); $inverse = false; if ($end < $start) { $start = $end; $end = $this; $inverse = true; } // Hack around PHP's DatePeriod not counting equal dates at midnight as // within the range. Sadly INCLUDE_END_DATE doesn't land until 8.2 $endTime = $end->native->modify('+1 second'); $period = new DatePeriod($start->native, $interval, $endTime, $options); $vals = array_filter(iterator_to_array($period), function (DateTimeInterface $date) use ($callback) { return $callback(static::parse($date)); }); $diff = count($vals); return $inverse && !$absolute ? -$diff : $diff; } /** * Get the difference in years * * @param \Cake\Chronos\ChronosDate|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInYears(?ChronosDate $other = null, bool $absolute = true): int { $diff = $this->diff($other ?? new static(new Chronos()), $absolute); return $diff->invert ? -$diff->y : $diff->y; } /** * Get the difference in months * * @param \Cake\Chronos\ChronosDate|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInMonths(?ChronosDate $other = null, bool $absolute = true): int { $diff = $this->diff($other ?? new static(Chronos::now()), $absolute); $months = $diff->y * Chronos::MONTHS_PER_YEAR + $diff->m; return $diff->invert ? -$months : $months; } /** * Get the difference in weeks * * @param \Cake\Chronos\ChronosDate|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInWeeks(?ChronosDate $other = null, bool $absolute = true): int { return (int)($this->diffInDays($other, $absolute) / Chronos::DAYS_PER_WEEK); } /** * Get the difference in days * * @param \Cake\Chronos\ChronosDate|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInDays(?ChronosDate $other = null, bool $absolute = true): int { $diff = $this->diff($other ?? new static(Chronos::now()), $absolute); return $diff->invert ? -(int)$diff->days : (int)$diff->days; } /** * Get the difference in days using a filter callable * * @param callable $callback The callback to use for filtering. * @param \Cake\Chronos\ChronosDate|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @param int $options DatePeriod options, {@see https://www.php.net/manual/en/class.dateperiod.php} * @return int */ public function diffInDaysFiltered( callable $callback, ?ChronosDate $other = null, bool $absolute = true, int $options = 0 ): int { return $this->diffFiltered(new DateInterval('P1D'), $callback, $other, $absolute, $options); } /** * Get the difference in weekdays * * @param \Cake\Chronos\ChronosDate|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @param int $options DatePeriod options, {@see https://www.php.net/manual/en/class.dateperiod.php} * @return int */ public function diffInWeekdays(?ChronosDate $other = null, bool $absolute = true, int $options = 0): int { return $this->diffInDaysFiltered(function (ChronosDate $date) { return $date->isWeekday(); }, $other, $absolute, $options); } /** * Get the difference in weekend days using a filter * * @param \Cake\Chronos\ChronosDate|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @param int $options DatePeriod options, {@see https://www.php.net/manual/en/class.dateperiod.php} * @return int */ public function diffInWeekendDays(?ChronosDate $other = null, bool $absolute = true, int $options = 0): int { return $this->diffInDaysFiltered(function (ChronosDate $date) { return $date->isWeekend(); }, $other, $absolute, $options); } /** * Get the difference in a human readable format. * * When comparing a value in the past to default now: * 5 months ago * * When comparing a value in the future to default now: * 5 months from now * * When comparing a value in the past to another value: * 5 months before * * When comparing a value in the future to another value: * 5 months after * * @param \Cake\Chronos\ChronosDate|null $other The datetime to compare with. * @param bool $absolute removes difference modifiers ago, after, etc * @return string */ public function diffForHumans(?ChronosDate $other = null, bool $absolute = false): string { return static::diffFormatter()->diffForHumans($this, $other, $absolute); } /** * Returns the date as a `DateTimeImmutable` instance at midnight. * * @param \DateTimeZone|string|null $timezone Time zone the DateTimeImmutable instance will be in * @return \DateTimeImmutable */ public function toDateTimeImmutable(DateTimeZone|string|null $timezone = null): DateTimeImmutable { if ($timezone === null) { return $this->native; } $timezone = is_string($timezone) ? new DateTimeZone($timezone) : $timezone; return new DateTimeImmutable($this->native->format('Y-m-d H:i:s.u'), $timezone); } /** * Returns the date as a `DateTimeImmutable` instance at midnight. * * Alias of `toDateTimeImmutable()`. * * @param \DateTimeZone|string|null $timezone Time zone the DateTimeImmutable instance will be in * @return \DateTimeImmutable */ public function toNative(DateTimeZone|string|null $timezone = null): DateTimeImmutable { return $this->toDateTimeImmutable($timezone); } /** * Get a part of the object * * @param string $name The property name to read. * @return string|float|int|bool The property value. * @throws \InvalidArgumentException */ public function __get(string $name): string|float|int|bool { static $formats = [ 'year' => 'Y', 'yearIso' => 'o', 'month' => 'n', 'day' => 'j', 'dayOfWeek' => 'N', 'dayOfYear' => 'z', 'weekOfYear' => 'W', 'daysInMonth' => 't', ]; switch (true) { case isset($formats[$name]): return (int)$this->format($formats[$name]); case $name === 'dayOfWeekName': return $this->format('l'); case $name === 'weekOfMonth': return (int)ceil($this->day / Chronos::DAYS_PER_WEEK); case $name === 'age': return $this->diffInYears(); case $name === 'quarter': return (int)ceil($this->month / 3); case $name === 'half': return $this->month <= 6 ? 1 : 2; default: throw new InvalidArgumentException(sprintf('Unknown getter `%s`', $name)); } } /** * Check if an attribute exists on the object * * @param string $name The property name to check. * @return bool Whether the property exists. */ public function __isset(string $name): bool { try { $this->__get($name); } catch (InvalidArgumentException $e) { return false; } return true; } /** * Return properties for debugging. * * @return array */ public function __debugInfo(): array { $properties = [ 'hasFixedNow' => Chronos::hasTestNow(), 'date' => $this->format('Y-m-d'), ]; return $properties; } }