* @link https://cakephp.org CakePHP(tm) Project * @license https://www.opensource.org/licenses/mit-license.php MIT License */ namespace Cake\Chronos; use DateInterval; use DatePeriod; use DateTimeImmutable; use DateTimeInterface; use DateTimeZone; use InvalidArgumentException; use RuntimeException; use Stringable; /** * An Immutable extension on the native DateTime object. * * Adds a number of convenience APIs methods and the ability * to easily convert into a mutable object. * * @property-read int $year * @property-read int $yearIso * @property-read int<1, 12> $month * @property-read int<1, 31> $day * @property-read int<0, 23> $hour * @property-read int<0, 59> $minute * @property-read int<0, 59> $second * @property-read int<0, 999999> $micro * @property-read int<0, 999999> $microsecond * @property-read int $timestamp seconds since the Unix Epoch * @property-read \DateTimeZone $timezone the current timezone * @property-read \DateTimeZone $tz alias of timezone * @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. * @property-read int $offset the timezone offset in seconds from UTC * @property-read int $offsetHours the timezone offset in hours from UTC * @property-read bool $dst daylight savings time indicator, true if DST, false otherwise * @property-read bool $local checks if the timezone is local, true if local, false otherwise * @property-read bool $utc checks if the timezone is UTC, true if UTC, false otherwise * @property-read string $timezoneName * @property-read string $tzName * @psalm-immutable * @psalm-consistent-constructor */ class Chronos extends DateTimeImmutable implements Stringable { use FormattingTrait; /** * @var int */ public const MONDAY = 1; /** * @var int */ public const TUESDAY = 2; /** * @var int */ public const WEDNESDAY = 3; /** * @var int */ public const THURSDAY = 4; /** * @var int */ public const FRIDAY = 5; /** * @var int */ public const SATURDAY = 6; /** * @var int */ public const SUNDAY = 7; /** * @var int */ public const YEARS_PER_CENTURY = 100; /** * @var int */ public const YEARS_PER_DECADE = 10; /** * @var int */ public const MONTHS_PER_YEAR = 12; /** * @var int */ public const MONTHS_PER_QUARTER = 3; /** * @var int */ public const WEEKS_PER_YEAR = 52; /** * @var int */ public const DAYS_PER_WEEK = 7; /** * @var int */ public const HOURS_PER_DAY = 24; /** * @var int */ public const MINUTES_PER_HOUR = 60; /** * @var int */ public const SECONDS_PER_MINUTE = 60; /** * Default format to use for __toString method when type juggling occurs. * * @var string */ public const DEFAULT_TO_STRING_FORMAT = 'Y-m-d H:i:s'; /** * A test Chronos instance to be returned when now instances are created * * There is a single test now for all date/time classes provided by Chronos. * This aims to emulate stubbing out 'now' which is a single global fact. * * @var \Cake\Chronos\Chronos|null */ protected static ?Chronos $testNow = null; /** * Format to use for __toString method when type juggling occurs. * * @var string */ protected static string $toStringFormat = self::DEFAULT_TO_STRING_FORMAT; /** * Days of weekend * * @var array */ protected static array $weekendDays = [Chronos::SATURDAY, Chronos::SUNDAY]; /** * 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', ]; /** * First day of week * * @var int */ protected static int $weekStartsAt = Chronos::MONDAY; /** * Last day of week * * @var int */ protected static int $weekEndsAt = Chronos::SUNDAY; /** * Instance of the diff formatting object. * * @var \Cake\Chronos\DifferenceFormatterInterface|null */ protected static ?DifferenceFormatterInterface $diffFormatter = null; /** * Regex for relative period. * * @var string */ // phpcs:disable Generic.Files.LineLength.TooLong protected static string $relativePattern = '/this|next|last|tomorrow|yesterday|midnight|today|[+-]|first|last|ago/i'; /** * Errors from last time createFromFormat() was called. * * @var array|false */ protected static array|false $lastErrors = false; /** * Create a new Chronos instance. * * Please see the testing aids section (specifically static::setTestNow()) * for more on the possibility of this constructor returning a test instance. * * @param \Cake\Chronos\ChronosDate|\Cake\Chronos\ChronosTime|\DateTimeInterface|string|int|null $time Fixed or relative time * @param \DateTimeZone|string|null $timezone The timezone for the instance */ public function __construct( ChronosDate|ChronosTime|DateTimeInterface|string|int|null $time = 'now', DateTimeZone|string|null $timezone = null ) { if (is_int($time) || (is_string($time) && ctype_digit($time))) { parent::__construct("@{$time}"); return; } if ($timezone !== null) { $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); } if (is_object($time)) { if ($time instanceof DateTimeInterface) { $timezone = $time->getTimezone(); } $time = $time->format('Y-m-d H:i:s.u'); } $testNow = static::getTestNow(); if ($testNow === null) { parent::__construct($time ?? 'now', $timezone); return; } $relative = static::hasRelativeKeywords($time); if ($time && $time !== 'now' && !$relative) { parent::__construct($time, $timezone); return; } $testNow = clone $testNow; $relativeTime = self::isTimeExpression($time); if (!$relativeTime && $timezone !== $testNow->getTimezone()) { $testNow = $testNow->setTimezone($timezone ?? date_default_timezone_get()); } if ($relative) { $testNow = $testNow->modify($time ?? 'now'); } parent::__construct($testNow->format('Y-m-d H:i:s.u'), $timezone); } /** * Set a Chronos instance (real or mock) to be returned when a "now" * instance is created. The provided instance will be returned * specifically under the following conditions: * - A call to the static now() method, ex. Chronos::now() * - When a null (or blank string) is passed to the constructor or parse(), ex. new Chronos(null) * - When the string "now" is passed to the constructor or parse(), ex. new Chronos('now') * - When a string containing the desired time is passed to Chronos::parse() * * Note the timezone parameter was left out of the examples above and * has no affect as the mock value will be returned regardless of its value. * * To clear the test instance call this method using the default * parameter of null. * * @param \Cake\Chronos\Chronos|string|null $testNow The instance to use for all future instances. * @return void */ public static function setTestNow(Chronos|string|null $testNow = null): void { static::$testNow = is_string($testNow) ? static::parse($testNow) : $testNow; } /** * Get the Chronos instance (real or mock) to be returned when a "now" * instance is created. * * @return \Cake\Chronos\Chronos|null The current instance used for testing */ public static function getTestNow(): ?Chronos { return static::$testNow; } /** * Determine if there is a valid test instance set. A valid test instance * is anything that is not null. * * @return bool True if there is a test instance, otherwise false */ public static function hasTestNow(): bool { return static::$testNow !== null; } /** * Determine if there is just a time in the time string * * @param string|null $time The time string to check. * @return bool true if there is a keyword, otherwise false */ private static function isTimeExpression(?string $time): bool { // Just a time if (is_string($time) && preg_match('/^[0-2]?[0-9]:[0-5][0-9](?::[0-5][0-9](?:\.[0-9]{1,6})?)?$/', $time)) { return true; } return false; } /** * Determine if there is a relative keyword in the time string, this is to * create dates relative to now for test instances. e.g.: next tuesday * * @param string|null $time The time string to check. * @return bool true if there is a keyword, otherwise false */ public static function hasRelativeKeywords(?string $time): bool { if (self::isTimeExpression($time)) { return true; } // skip common format with a '-' in it if ($time && preg_match('/[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}/', $time) !== 1) { return preg_match(static::$relativePattern, $time) > 0; } return false; } /** * Get weekend days * * @return array */ public static function getWeekendDays(): array { return static::$weekendDays; } /** * Set weekend days * * @param array $days Which days are 'weekends'. * @return void */ public static function setWeekendDays(array $days): void { static::$weekendDays = $days; } /** * Get the first day of week * * @return int */ public static function getWeekStartsAt(): int { return static::$weekStartsAt; } /** * Set the first day of week * * @param int $day The day the week starts with. * @return void */ public static function setWeekStartsAt(int $day): void { static::$weekStartsAt = $day; } /** * Get the last day of week * * @return int */ public static function getWeekEndsAt(): int { return static::$weekEndsAt; } /** * Set the last day of week * * @param int $day The day the week ends with. * @return void */ public static function setWeekEndsAt(int $day): void { static::$weekEndsAt = $day; } /** * 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; } /** * Create an instance from a DateTimeInterface * * @param \DateTimeInterface $other The datetime instance to convert. * @return static */ public static function instance(DateTimeInterface $other): static { return new static($other); } /** * 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|\Cake\Chronos\ChronosTime|\DateTimeInterface|string|int|null $time The strtotime compatible string to parse * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name. * @return static */ public static function parse( ChronosDate|ChronosTime|DateTimeInterface|string|int|null $time = 'now', DateTimeZone|string|null $timezone = null ): static { return new static($time, $timezone); } /** * Get an instance for the current date and time * * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name. * @return static */ public static function now(DateTimeZone|string|null $timezone = null): static { return new static('now', $timezone); } /** * Create an instance for today * * @param \DateTimeZone|string|null $timezone The timezone to use. * @return static */ public static function today(DateTimeZone|string|null $timezone = null): static { return new static('midnight', $timezone); } /** * Create an instance for tomorrow * * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name the new instance should use. * @return static */ public static function tomorrow(DateTimeZone|string|null $timezone = null): static { return new static('tomorrow, midnight', $timezone); } /** * Create an instance for yesterday * * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name the new instance should use. * @return static */ public static function yesterday(DateTimeZone|string|null $timezone = null): static { return new static('yesterday, midnight', $timezone); } /** * Create an instance for the greatest supported date. * * @return static */ public static function maxValue(): static { return static::createFromTimestamp(PHP_INT_MAX); } /** * Create an instance for the lowest supported date. * * @return static */ public static function minValue(): static { $max = PHP_INT_SIZE === 4 ? PHP_INT_MAX : PHP_INT_MAX / 10; return static::createFromTimestamp(~$max); } /** * Create an instance from a specific date and time. * * If any of $year, $month or $day are set to null their now() values * will be used. * * If $hour is null it will be set to its now() value and the default values * for $minute, $second and $microsecond will be their now() values. * If $hour is not null then the default values for $minute, $second * and $microsecond will be 0. * * @param int|null $year The year to create an instance with. * @param int|null $month The month to create an instance with. * @param int|null $day The day to create an instance with. * @param int|null $hour The hour to create an instance with. * @param int|null $minute The minute to create an instance with. * @param int|null $second The second to create an instance with. * @param int|null $microsecond The microsecond to create an instance with. * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name the new instance should use. * @return static */ public static function create( ?int $year = null, ?int $month = null, ?int $day = null, ?int $hour = null, ?int $minute = null, ?int $second = null, ?int $microsecond = null, DateTimeZone|string|null $timezone = null ): static { $now = static::now(); $year = $year ?? (int)$now->format('Y'); $month = $month ?? $now->format('m'); $day = $day ?? $now->format('d'); if ($hour === null) { $hour = $now->format('H'); $minute = $minute ?? $now->format('i'); $second = $second ?? $now->format('s'); $microsecond = $microsecond ?? $now->format('u'); } else { $minute = $minute ?? 0; $second = $second ?? 0; $microsecond = $microsecond ?? 0; } $instance = static::createFromFormat( 'Y-m-d H:i:s.u', sprintf('%s-%s-%s %s:%02s:%02s.%06s', 0, $month, $day, $hour, $minute, $second, $microsecond), $timezone ); return $instance->addYears($year); } /** * Create an instance from just a date. The time portion is set to now. * * @param int|null $year The year to create an instance with. * @param int|null $month The month to create an instance with. * @param int|null $day The day to create an instance with. * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name the new instance should use. * @return static */ public static function createFromDate( ?int $year = null, ?int $month = null, ?int $day = null, DateTimeZone|string|null $timezone = null ): static { return static::create($year, $month, $day, null, null, null, null, $timezone); } /** * Create an instance from just a time. The date portion is set to today. * * @param int|null $hour The hour to create an instance with. * @param int|null $minute The minute to create an instance with. * @param int|null $second The second to create an instance with. * @param int|null $microsecond The microsecond to create an instance with. * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name the new instance should use. * @return static */ public static function createFromTime( ?int $hour = null, ?int $minute = null, ?int $second = null, ?int $microsecond = null, DateTimeZone|string|null $timezone = null ): static { return static::create(null, null, null, $hour, $minute, $second, $microsecond, $timezone); } /** * Create an instance from a specific format * * @param string $format The date() compatible format string. * @param string $time The formatted date string to interpret. * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name the new instance should use. * @return static * @throws \InvalidArgumentException */ public static function createFromFormat( string $format, string $time, DateTimeZone|string|null $timezone = null ): static { if ($timezone !== null) { $dateTime = parent::createFromFormat($format, $time, $timezone ? static::safeCreateDateTimeZone($timezone) : null); } else { $dateTime = parent::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 $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 and time values. * * The 'year', 'month' and 'day' values must all be set for a date. The time * values all default to 0. * * The 'timezone' value can be any format supported by `\DateTimeZone`. * * Allowed values: * - year * - month * - day * - hour * - minute * - second * - microsecond * - meridian ('am' or 'pm') * - timezone * * @param array $values Array of date and time values. * @return static */ public static function createFromArray(array $values): static { $values += ['hour' => 0, 'minute' => 0, 'second' => 0, 'microsecond' => 0, 'timezone' => null]; $formatted = ''; if ( isset($values['year'], $values['month'], $values['day']) && ( is_numeric($values['year']) && is_numeric($values['month']) && is_numeric($values['day']) ) ) { $formatted .= sprintf('%04d-%02d-%02d ', $values['year'], $values['month'], $values['day']); } if (isset($values['meridian']) && (int)$values['hour'] === 12) { $values['hour'] = 0; } if (isset($values['meridian'])) { $values['hour'] = strtolower((string)$values['meridian']) === 'am' ? (int)$values['hour'] : (int)$values['hour'] + 12; } $formatted .= sprintf( '%02d:%02d:%02d.%06d', $values['hour'], $values['minute'], $values['second'], $values['microsecond'] ); assert(!is_int($values['timezone']), 'Timezone cannot be of type `int`'); return static::parse($formatted, $values['timezone']); } /** * Create an instance from a timestamp * * @param float|int $timestamp The timestamp to create an instance from. * @param \DateTimeZone|string|null $timezone The DateTimeZone object or timezone name the new instance should use. * @return static */ public static function createFromTimestamp(float|int $timestamp, DateTimeZone|string|null $timezone = null): static { $instance = PHP_VERSION_ID >= 80400 ? parent::createFromTimestamp($timestamp) : new static('@' . $timestamp); return $timezone ? $instance->setTimezone($timezone) : $instance; } /** * Creates a DateTimeZone from a string or a DateTimeZone * * @param \DateTimeZone|string|null $object The value to convert. * @return \DateTimeZone * @throws \InvalidArgumentException */ protected static function safeCreateDateTimeZone(DateTimeZone|string|null $object): DateTimeZone { if ($object === null) { return new DateTimeZone(date_default_timezone_get()); } if ($object instanceof DateTimeZone) { return $object; } return new DateTimeZone($object); } /** * Create a new DateInterval instance from specified values. * * @param int|null $years The year to use. * @param int|null $months The month to use. * @param int|null $weeks The week to use. * @param int|null $days The day to use. * @param int|null $hours The hours to use. * @param int|null $minutes The minutes to use. * @param int|null $seconds The seconds to use. * @param int|null $microseconds The microseconds to use. * @return \DateInterval */ public static function createInterval( ?int $years = null, ?int $months = null, ?int $weeks = null, ?int $days = null, ?int $hours = null, ?int $minutes = null, ?int $seconds = null, ?int $microseconds = null, ): DateInterval { $spec = 'P'; $rollover = static::rolloverTime($microseconds, 1_000_000); $seconds = $seconds === null ? $rollover : $seconds + (int)$rollover; $rollover = static::rolloverTime($seconds, 60); $minutes = $minutes === null ? $rollover : $minutes + (int)$rollover; $rollover = static::rolloverTime($minutes, 60); $hours = $hours === null ? $rollover : $hours + (int)$rollover; $rollover = static::rolloverTime($hours, 24); $days = $days === null ? $rollover : $days + (int)$rollover; if ($years) { $spec .= $years . 'Y'; } if ($months) { $spec .= $months . 'M'; } if ($weeks) { $spec .= $weeks . 'W'; } if ($days) { $spec .= $days . 'D'; } if ($hours || $minutes || $seconds) { $spec .= 'T'; if ($hours) { $spec .= $hours . 'H'; } if ($minutes) { $spec .= $minutes . 'M'; } if ($seconds) { $spec .= $seconds . 'S'; } } if ($microseconds && $spec === 'P') { $spec .= 'T0S'; } $instance = new DateInterval($spec); if ($microseconds) { $instance->f = $microseconds / 1000000; } return $instance; } /** * Updates value to remaininger and returns rollover value for time * unit or null if no rollover. * * @param int|null $value Time unit value * @param int $max Time unit max value * @return int|null */ protected static function rolloverTime(?int &$value, int $max): ?int { if ($value === null || $value < $max) { return null; } $rollover = intdiv($value, $max); $value = $value % $max; return $rollover; } /** * Sets the date and time. * * @param int $year The year to set. * @param int $month The month to set. * @param int $day The day to set. * @param int $hour The hour to set. * @param int $minute The minute to set. * @param int $second The second to set. * @return static */ public function setDateTime( int $year, int $month, int $day, int $hour, int $minute, int $second = 0 ): static { return $this->setDate($year, $month, $day)->setTime($hour, $minute, $second); } /** * 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 { return parent::setDate($year, $month, $day); } /** * 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 { return parent::setISODate($year, $week, $dayOfWeek); } /** * Sets the time. * * @param int $hours Hours of the time * @param int $minutes Minutes of the time * @param int $seconds Seconds of the time * @param int $microseconds Microseconds of the time * @return static */ public function setTime(int $hours, int $minutes, int $seconds = 0, int $microseconds = 0): static { return parent::setTime($hours, $minutes, $seconds, $microseconds); } /** * Creates a new instance with date modified according to DateTimeImmutable::modifier(). * * @param string $modifier Date modifier * @return static * @throws \InvalidArgumentException * @see https://www.php.net/manual/en/datetimeimmutable.modify.php */ public function modify(string $modifier): static { $new = parent::modify($modifier); if ($new === false) { throw new InvalidArgumentException(sprintf('Unable to modify date using `%s`', $modifier)); } return $new; } /** * Returns the difference between this instance and target. * * @param \DateTimeInterface $target Target instance * @param bool $absolute Whether the interval is forced to be positive * @return \DateInterval */ public function diff(DateTimeInterface $target, bool $absolute = false): DateInterval { return parent::diff($target, $absolute); } /** * Returns formatted date string according to DateTimeImmutable::format(). * * @param string $format String format * @return string */ public function format(string $format): string { return parent::format($format); } /** * Returns the timezone offset. * * @return int */ public function getOffset(): int { return parent::getOffset(); } /** * Sets the date and time based on a Unix timestamp. * * @param int $timestamp Unix timestamp representing the date * @return static */ public function setTimestamp(int $timestamp): static { return parent::setTimestamp($timestamp); } /** * Gets the Unix timestamp for this instance. * * @return int */ public function getTimestamp(): int { return parent::getTimestamp(); } /** * Set the instance's timezone from a string or object * * @param \DateTimeZone|string $value The DateTimeZone object or timezone name to use. * @return static */ public function setTimezone(DateTimeZone|string $value): static { return parent::setTimezone(static::safeCreateDateTimeZone($value)); } /** * Return time zone set for this instance. * * @return \DateTimeZone */ public function getTimezone(): DateTimeZone { $tz = parent::getTimezone(); if ($tz === false) { throw new RuntimeException('Time zone could not be retrieved.'); } return $tz; } /** * Set the time by time string * * @param string $time Time as string. * @return static */ public function setTimeFromTimeString(string $time): static { $time = explode(':', $time); $hour = $time[0]; $minute = $time[1] ?? 0; $second = $time[2] ?? 0; return $this->setTime((int)$hour, (int)$minute, (int)$second); } /** * Set the instance's timestamp * * @param int $value The timestamp value to set. * @return static */ public function timestamp(int $value): static { return $this->setTimestamp($value); } /** * 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); } /** * Set the instance's hour * * @param int $value The hour value. * @return static */ public function hour(int $value): static { return $this->setTime($value, $this->minute, $this->second); } /** * Set the instance's minute * * @param int $value The minute value. * @return static */ public function minute(int $value): static { return $this->setTime($this->hour, $value, $this->second); } /** * Set the instance's second * * @param int $value The seconds value. * @return static */ public function second(int $value): static { return $this->setTime($this->hour, $this->minute, $value); } /** * Set the instance's microsecond * * @param int $value The microsecond value. * @return static */ public function microsecond(int $value): static { return $this->setTime($this->hour, $this->minute, $this->second, $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); } /** * Add hours to the instance. Positive $value travels forward while * negative $value travels into the past. * * @param int $value The number of hours to add. * @return static */ public function addHours(int $value): static { return $this->modify("$value hour"); } /** * Remove hours from the instance * * @param int $value The number of hours to remove. * @return static */ public function subHours(int $value): static { return $this->addHours(-$value); } /** * Add minutes to the instance. Positive $value travels forward while * negative $value travels into the past. * * @param int $value The number of minutes to add. * @return static */ public function addMinutes(int $value): static { return $this->modify("$value minute"); } /** * Remove minutes from the instance * * @param int $value The number of minutes to remove. * @return static */ public function subMinutes(int $value): static { return $this->addMinutes(-$value); } /** * Add seconds to the instance. Positive $value travels forward while * negative $value travels into the past. * * @param int $value The number of seconds to add. * @return static */ public function addSeconds(int $value): static { return $this->modify("$value second"); } /** * Remove seconds from the instance * * @param int $value The number of seconds to remove. * @return static */ public function subSeconds(int $value): static { return $this->addSeconds(-$value); } /** * Sets the time to 00:00:00 * * @return static */ public function startOfDay(): static { return $this->modify('midnight'); } /** * Sets the time to 23:59:59 or 23:59:59.999999 * if `$microseconds` is true. * * @param bool $microseconds Whether to set microseconds * @return static */ public function endOfDay(bool $microseconds = false): static { if ($microseconds) { return $this->modify('23:59:59.999999'); } return $this->modify('23:59:59'); } /** * Sets the date to the first day of the month and the time to 00:00:00 * * @return static */ public function startOfMonth(): static { return $this->modify('first day of this month midnight'); } /** * Sets the date to end of the month and time to 23:59:59 * * @return static */ public function endOfMonth(): static { return $this->modify('last day of this month, 23:59:59'); } /** * Sets the date to the first day of the year and the time to 00:00:00 * * @return static */ public function startOfYear(): static { return $this->modify('first day of january midnight'); } /** * Sets the date to end of the year and time to 23:59:59 * * @return static */ public function endOfYear(): static { return $this->modify('last day of december, 23:59:59'); } /** * Sets the date to the first day of the decade and the time to 00:00:00 * * @return static */ public function startOfDecade(): static { $year = $this->year - $this->year % Chronos::YEARS_PER_DECADE; return $this->modify("first day of january $year, midnight"); } /** * Sets the date to end of the decade and time to 23:59:59 * * @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, 23:59:59"); } /** * Sets the date to the first day of the century and the time to 00:00:00 * * @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, midnight"); } /** * Sets 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, 23:59:59"); } /** * Sets the date to the first day of week (defined in $weekStartsAt) and the time to 00:00:00 * * @return static */ public function startOfWeek(): static { $dateTime = $this; if ($dateTime->dayOfWeek !== static::$weekStartsAt) { $dateTime = $dateTime->previous(static::$weekStartsAt); } return $dateTime->startOfDay(); } /** * Sets 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 !== static::$weekEndsAt) { $dateTime = $dateTime->next(static::$weekEndsAt); } return $dateTime->endOfDay(); } /** * 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, midnight"); } /** * 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, midnight"); } /** * 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, midnight"); } /** * 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, midnight"); } /** * 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, midnight"); } /** * 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, midnight"); } /** * 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 \DateTimeInterface $other The instance to compare with. * @return bool */ public function equals(DateTimeInterface $other): bool { return $this == $other; } /** * Determines if the instance is not equal to another * * @param \DateTimeInterface $other The instance to compare with. * @return bool */ public function notEquals(DateTimeInterface $other): bool { return !$this->equals($other); } /** * Determines if the instance is greater (after) than another * * @param \DateTimeInterface $other The instance to compare with. * @return bool */ public function greaterThan(DateTimeInterface $other): bool { return $this > $other; } /** * Determines if the instance is greater (after) than or equal to another * * @param \DateTimeInterface $other The instance to compare with. * @return bool */ public function greaterThanOrEquals(DateTimeInterface $other): bool { return $this >= $other; } /** * Determines if the instance is less (before) than another * * @param \DateTimeInterface $other The instance to compare with. * @return bool */ public function lessThan(DateTimeInterface $other): bool { return $this < $other; } /** * Determines if the instance is less (before) or equal to another * * @param \DateTimeInterface $other The instance to compare with. * @return bool */ public function lessThanOrEquals(DateTimeInterface $other): bool { return $this <= $other; } /** * Determines if the instance is between two others * * @param \DateTimeInterface $start Start of target range * @param \DateTimeInterface $end End of target range * @param bool $equals Whether to include the beginning and end of range * @return bool */ public function between(DateTimeInterface $start, DateTimeInterface $end, bool $equals = true): bool { if ($start > $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 \DateTimeInterface $first The instance to compare with. * @param \DateTimeInterface $second The instance to compare with. * @param \DateTimeInterface ...$others Others instances to compare with. * @return static */ public function closest(DateTimeInterface $first, DateTimeInterface $second, DateTimeInterface ...$others): static { $winner = $first; $closestDiffInSeconds = $this->diffInSeconds($first); foreach ([$second, ...$others] as $other) { $otherDiffInSeconds = $this->diffInSeconds($other); if ($otherDiffInSeconds < $closestDiffInSeconds) { $winner = $other; $closestDiffInSeconds = $otherDiffInSeconds; } } if ($winner instanceof static) { return $winner; } return new static($winner); } /** * Get the farthest date from the instance. * * @param \DateTimeInterface $first The instance to compare with. * @param \DateTimeInterface $second The instance to compare with. * @param \DateTimeInterface ...$others Others instances to compare with. * @return static */ public function farthest(DateTimeInterface $first, DateTimeInterface $second, DateTimeInterface ...$others): static { $winner = $first; $farthestDiffInSeconds = $this->diffInSeconds($first); foreach ([$second, ...$others] as $other) { $otherDiffInSeconds = $this->diffInSeconds($other); if ($otherDiffInSeconds > $farthestDiffInSeconds) { $winner = $other; $farthestDiffInSeconds = $otherDiffInSeconds; } } if ($winner instanceof static) { return $winner; } return new static($winner); } /** * Get the minimum instance between a given instance (default now) and the current instance. * * @param \DateTimeInterface|null $other The instance to compare with. * @return static */ public function min(?DateTimeInterface $other = null): static { $other = $other ?? static::now($this->tz); $winner = $this->lessThan($other) ? $this : $other; if ($winner instanceof static) { return $winner; } return new static($winner); } /** * Get the maximum instance between a given instance (default now) and the current instance. * * @param \DateTimeInterface|null $other The instance to compare with. * @return static */ public function max(?DateTimeInterface $other = null): static { $other = $other ?? static::now($this->tz); $winner = $this->greaterThan($other) ? $this : $other; if ($winner instanceof static) { return $winner; } return new static($winner); } /** * Modify the current instance to the average of a given instance (default now) and the current instance. * * @param \DateTimeInterface|null $other The instance to compare with. * @return static */ public function average(?DateTimeInterface $other = null): static { $other ??= static::now($this->tz); return $this->addSeconds((int)($this->diffInSeconds($other, false) / 2)); } /** * 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 * * @return bool */ public function isYesterday(): bool { return $this->toDateString() === static::yesterday($this->tz)->toDateString(); } /** * Determines if the instance is today * * @return bool */ public function isToday(): bool { return $this->toDateString() === static::now($this->tz)->toDateString(); } /** * Determines if the instance is tomorrow * * @return bool */ public function isTomorrow(): bool { return $this->toDateString() === static::tomorrow($this->tz)->toDateString(); } /** * Determines if the instance is within the next week * * @return bool */ public function isNextWeek(): bool { return $this->format('W o') === static::now($this->tz)->addWeeks(1)->format('W o'); } /** * Determines if the instance is within the last week * * @return bool */ public function isLastWeek(): bool { return $this->format('W o') === static::now($this->tz)->subWeeks(1)->format('W o'); } /** * Determines if the instance is within the next month * * @return bool */ public function isNextMonth(): bool { return $this->format('m Y') === static::now($this->tz)->addMonths(1)->format('m Y'); } /** * Determines if the instance is within the last month * * @return bool */ public function isLastMonth(): bool { return $this->format('m Y') === static::now($this->tz)->subMonths(1)->format('m Y'); } /** * Determines if the instance is within the next year * * @return bool */ public function isNextYear(): bool { return $this->year === static::now($this->tz)->addYears(1)->year; } /** * Determines if the instance is within the last year * * @return bool */ public function isLastYear(): bool { return $this->year === static::now($this->tz)->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 * * @return bool */ public function isFuture(): bool { return $this->greaterThan(static::now($this->tz)); } /** * Determines if the instance is in the past, ie. less (before) than now * * @return bool */ public function isPast(): bool { return $this->lessThan(static::now($this->tz)); } /** * Determines if the instance is a leap year * * @return bool */ public function isLeapYear(): bool { return $this->format('L') === '1'; } /** * Checks if the passed in date is the same day as the instance current day. * * @param \DateTimeInterface $other The instance to check against. * @return bool */ public function isSameDay(DateTimeInterface $other): bool { if (!$other instanceof static) { $other = new static($other); } return $this->toDateString() === $other->toDateString(); } /** * Returns whether the passed in date is the same month and year. * * @param \DateTimeInterface $other The instance to check against. * @return bool */ public function isSameMonth(DateTimeInterface $other): bool { return $this->format('Y-m') === $other->format('Y-m'); } /** * Returns whether passed in date is the same year. * * @param \DateTimeInterface $other The instance to check against. * @return bool */ public function isSameYear(DateTimeInterface $other): bool { return $this->format('Y') === $other->format('Y'); } /** * 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 if this object represents a date within the current week * * @return bool */ public function isThisWeek(): bool { return static::now($this->getTimezone())->format('W o') === $this->format('W o'); } /** * Returns true if this object represents a date within the current month * * @return bool */ public function isThisMonth(): bool { return static::now($this->getTimezone())->format('m Y') === $this->format('m Y'); } /** * Returns true if this object represents a date within the current year * * @return bool */ public function isThisYear(): bool { return static::now($this->getTimezone())->format('Y') === $this->format('Y'); } /** * Check if its the birthday. Compares the date/month values of the two dates. * * @param \DateTimeInterface|null $other The instance to compare with or null to use current day. * @return bool */ public function isBirthday(?DateTimeInterface $other = null): bool { $other ??= static::now($this->tz); return $this->format('md') === $other->format('md'); } /** * 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(); $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(); $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 \DateTimeInterface|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, ?DateTimeInterface $other = null, bool $absolute = true, int $options = 0 ): int { $start = $this; $end = $other ?? static::now($this->tz); $inverse = false; if ($end < $start) { $start = $end; $end = $this; $inverse = true; } $period = new DatePeriod($start, $interval, $end, $options); $vals = array_filter(iterator_to_array($period), function (DateTimeInterface $date) use ($callback) { return $callback(static::instance($date)); }); $diff = count($vals); return $inverse && !$absolute ? -$diff : $diff; } /** * Get the difference in years * * @param \DateTimeInterface|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInYears(?DateTimeInterface $other = null, bool $absolute = true): int { $diff = $this->diff($other ?? static::now($this->tz), $absolute); return $diff->invert ? -$diff->y : $diff->y; } /** * Get the difference in months * * @param \DateTimeInterface|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInMonths(?DateTimeInterface $other = null, bool $absolute = true): int { $diff = $this->diff($other ?? static::now($this->tz), $absolute); $months = $diff->y * Chronos::MONTHS_PER_YEAR + $diff->m; return $diff->invert ? -$months : $months; } /** * Get the difference in months ignoring the timezone. This means the months are calculated * in the specified timezone without converting to UTC first. This prevents the day from changing * which can change the month. * * For example, if comparing `2019-06-01 Asia/Tokyo` and `2019-10-01 Asia/Tokyo`, * the result would be 4 months instead of 3 when using normal `DateTime::diff()`. * * @param \DateTimeInterface|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInMonthsIgnoreTimezone(?DateTimeInterface $other = null, bool $absolute = true): int { $utcTz = new DateTimeZone('UTC'); $source = new static($this->format('Y-m-d H:i:s.u'), $utcTz); $other ??= static::now($this->tz); $other = new static($other->format('Y-m-d H:i:s.u'), $utcTz); return $source->diffInMonths($other, $absolute); } /** * Get the difference in weeks * * @param \DateTimeInterface|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInWeeks(?DateTimeInterface $other = null, bool $absolute = true): int { return (int)($this->diffInDays($other, $absolute) / Chronos::DAYS_PER_WEEK); } /** * Get the difference in days * * @param \DateTimeInterface|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInDays(?DateTimeInterface $other = null, bool $absolute = true): int { $diff = $this->diff($other ?? static::now($this->tz), $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 \DateTimeInterface|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, ?DateTimeInterface $other = null, bool $absolute = true, int $options = 0 ): int { return $this->diffFiltered(new DateInterval('P1D'), $callback, $other, $absolute, $options); } /** * Get the difference in hours using a filter callable * * @param callable $callback The callback to use for filtering. * @param \DateTimeInterface|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 diffInHoursFiltered( callable $callback, ?DateTimeInterface $other = null, bool $absolute = true, int $options = 0 ): int { return $this->diffFiltered(new DateInterval('PT1H'), $callback, $other, $absolute, $options); } /** * Get the difference in weekdays * * @param \DateTimeInterface|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(?DateTimeInterface $other = null, bool $absolute = true, int $options = 0): int { return $this->diffInDaysFiltered(function (Chronos $date) { return $date->isWeekday(); }, $other, $absolute, $options); } /** * Get the difference in weekend days using a filter * * @param \DateTimeInterface|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(?DateTimeInterface $other = null, bool $absolute = true, int $options = 0): int { return $this->diffInDaysFiltered(function (Chronos $date) { return $date->isWeekend(); }, $other, $absolute, $options); } /** * Get the difference in hours * * @param \DateTimeInterface|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInHours(?DateTimeInterface $other = null, bool $absolute = true): int { return (int)( $this->diffInSeconds($other, $absolute) / Chronos::SECONDS_PER_MINUTE / Chronos::MINUTES_PER_HOUR ); } /** * Get the difference in minutes * * @param \DateTimeInterface|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInMinutes(?DateTimeInterface $other = null, bool $absolute = true): int { return (int)($this->diffInSeconds($other, $absolute) / Chronos::SECONDS_PER_MINUTE); } /** * Get the difference in seconds * * @param \DateTimeInterface|null $other The instance to difference from. * @param bool $absolute Get the absolute of the difference * @return int */ public function diffInSeconds(?DateTimeInterface $other = null, bool $absolute = true): int { $other ??= static::now($this->tz); $value = $other->getTimestamp() - $this->getTimestamp(); return $absolute ? abs($value) : $value; } /** * The number of seconds since midnight. * * @return int */ public function secondsSinceMidnight(): int { return $this->diffInSeconds($this->startOfDay()); } /** * The number of seconds until 23:59:59. * * @return int */ public function secondsUntilEndOfDay(): int { return $this->diffInSeconds($this->endOfDay()); } /** * Convenience method for getting the remaining time from a given time. * * @param \DateTimeInterface $other The date to get the remaining time from. * @return \DateInterval|bool The DateInterval object representing the difference between the two dates or FALSE on failure. */ public static function fromNow(DateTimeInterface $other): DateInterval|bool { $timeNow = new static(); return $timeNow->diff($other); } /** * Get the difference in a human readable format. * * When comparing a value in the past to default now: * 1 hour ago * 5 months ago * * When comparing a value in the future to default now: * 1 hour from now * 5 months from now * * When comparing a value in the past to another value: * 1 hour before * 5 months before * * When comparing a value in the future to another value: * 1 hour after * 5 months after * * @param \DateTimeInterface|null $other The datetime to compare with. * @param bool $absolute removes time difference modifiers ago, after, etc * @return string */ public function diffForHumans(?DateTimeInterface $other = null, bool $absolute = false): string { return static::diffFormatter()->diffForHumans($this, $other, $absolute); } /** * Returns a DateTimeImmutable instance * * This method returns a PHP DateTimeImmutable without Chronos extensions. * * @return \DateTimeImmutable */ public function toNative(): DateTimeImmutable { return new DateTimeImmutable($this->format('Y-m-d H:i:s.u'), $this->getTimezone()); } /** * Get a part of the object * * @param string $name The property name to read. * @return \DateTimeZone|string|float|int|bool The property value. * @throws \InvalidArgumentException */ public function __get(string $name): string|float|int|bool|DateTimeZone { static $formats = [ 'year' => 'Y', 'yearIso' => 'o', 'month' => 'n', 'day' => 'j', 'hour' => 'G', 'minute' => 'i', 'second' => 's', 'micro' => 'u', 'microsecond' => 'u', 'dayOfWeek' => 'N', 'dayOfYear' => 'z', 'weekOfYear' => 'W', 'daysInMonth' => 't', 'timestamp' => 'U', ]; 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; case $name === 'offset': return $this->getOffset(); case $name === 'offsetHours': return $this->getOffset() / Chronos::SECONDS_PER_MINUTE / Chronos::MINUTES_PER_HOUR; case $name === 'dst': return $this->format('I') === '1'; case $name === 'local': return $this->offset === $this->setTimezone(date_default_timezone_get())->offset; case $name === 'utc': return $this->offset === 0; case $name === 'timezone' || $name === 'tz': return $this->getTimezone(); case $name === 'timezoneName' || $name === 'tzName': return $this->getTimezone()->getName(); 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 { /** @var \DateTimeZone $timezone */ $timezone = $this->getTimezone(); $properties = [ 'hasFixedNow' => static::hasTestNow(), 'time' => $this->format('Y-m-d H:i:s.u'), 'timezone' => $timezone->getName(), ]; return $properties; } }