<?php
declare(strict_types=1);

/**
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @copyright     Copyright (c) Brian Nesbitt <brian@nesbot.com>
 * @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
 * @immutable
 * @phpstan-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.
     */
    protected static ?Chronos $testNow = null;

    /**
     * Format to use for __toString method when type juggling occurs.
     */
    protected static string $toStringFormat = self::DEFAULT_TO_STRING_FORMAT;

    /**
     * Days of weekend
     */
    protected static array $weekendDays = [Chronos::SATURDAY, Chronos::SUNDAY];

    /**
     * Names of days of the week.
     */
    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
     */
    protected static int $weekStartsAt = Chronos::MONDAY;

    /**
     * Last day of week
     */
    protected static int $weekEndsAt = Chronos::SUNDAY;

    /**
     * Instance of the diff formatting object.
     */
    protected static ?DifferenceFormatterInterface $diffFormatter = null;

    /**
     * Regex for relative period.
     */
    // 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.
     */
    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 instanceof Chronos) {
            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 instanceof Chronos;
    }

    /**
     * Temporarily sets "now" to the given value and executes the callback.
     *
     * After the callback is executed, the previous value of "now" is restored.
     * This is useful for testing time-sensitive code without affecting other tests.
     *
     * ### Example:
     *
     * ```
     * $result = Chronos::withTestNow('2023-06-15 12:00:00', function () {
     *     return Chronos::now()->format('Y-m-d');
     * });
     * // $result === '2023-06-15'
     * ```
     *
     * @template T
     * @param \Cake\Chronos\Chronos|string|null $testNow The instance to use as "now".
     * @param callable(): T $callback The callback to execute.
     * @return T The return value of the callback.
     */
    public static function withTestNow(Chronos|string|null $testNow, callable $callback): mixed
    {
        $previous = static::getTestNow();
        static::setTestNow($testNow);

        try {
            return $callback();
        } finally {
            static::setTestNow($previous);
        }
    }

    /**
     * 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
        return is_string($time) && preg_match('/^[0-2]?\d:[0-5]\d(?::[0-5]\d(?:\.\d{1,6})?)?$/', $time);
    }

    /**
     * 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('/\d{4}-\d{1,2}-\d{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 instanceof DifferenceFormatterInterface) {
            if (!static::$diffFormatter instanceof DifferenceFormatterInterface) {
                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 ??= (int)$now->format('Y');
        $month ??= $now->format('m');
        $day ??= $now->format('d');

        if ($hour === null) {
            $hour = $now->format('H');
            $minute ??= $now->format('i');
            $second ??= $now->format('s');
            $microsecond ??= $now->format('u');
        } else {
            $minute ??= 0;
            $second ??= 0;
            $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<int|string> $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
     */
    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.
     *
     * Values that exceed their natural limits will automatically roll over
     * to the next higher unit. For example, 90 minutes becomes 1 hour and
     * 30 minutes, 25 hours becomes 1 day and 1 hour.
     *
     * Rollover cascades upward: microseconds -> seconds -> minutes -> hours -> days.
     * Years, months, and weeks do not roll over.
     *
     * Example:
     * ```
     * // 90 seconds becomes 1 minute 30 seconds
     * $interval = Chronos::createInterval(seconds: 90);
     *
     * // 25 hours becomes 1 day 1 hour
     * $interval = Chronos::createInterval(hours: 25);
     * ```
     *
     * @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 %= $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));
    }

    /**
     * Change the timezone while keeping the local time.
     *
     * Unlike `setTimezone()` which converts the time to the new timezone,
     * this method keeps the same wall clock time but changes the timezone.
     *
     * For example, if you have 10:00 AM in New York and shift to Chicago,
     * you'll get 10:00 AM in Chicago (not 9:00 AM as setTimezone would give).
     *
     * @param \DateTimeZone|string $timezone The new timezone
     * @return static
     */
    public function shiftTimezone(DateTimeZone|string $timezone): static
    {
        $timezone = static::safeCreateDateTimeZone($timezone);

        return new static($this->format('Y-m-d H:i:s.u'), $timezone);
    }

    /**
     * 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);
    }

    /**
     * Add hours to the instance using elapsed time.
     *
     * Unlike `addHours()` which uses wall clock time, this method
     * adds actual elapsed time by manipulating the Unix timestamp.
     * This is important when working across DST transitions where
     * wall clock time and elapsed time differ.
     *
     * @param int $value The number of hours to add.
     * @return static
     */
    public function addElapsedHours(int $value): static
    {
        return $this->setTimestamp($this->getTimestamp() + ($value * 3600));
    }

    /**
     * Remove hours from the instance using elapsed time.
     *
     * @param int $value The number of hours to remove.
     * @return static
     * @see addElapsedHours()
     */
    public function subElapsedHours(int $value): static
    {
        return $this->addElapsedHours(-$value);
    }

    /**
     * Add minutes to the instance using elapsed time.
     *
     * Unlike `addMinutes()` which uses wall clock time, this method
     * adds actual elapsed time by manipulating the Unix timestamp.
     * This is important when working across DST transitions where
     * wall clock time and elapsed time differ.
     *
     * @param int $value The number of minutes to add.
     * @return static
     */
    public function addElapsedMinutes(int $value): static
    {
        return $this->setTimestamp($this->getTimestamp() + ($value * 60));
    }

    /**
     * Remove minutes from the instance using elapsed time.
     *
     * @param int $value The number of minutes to remove.
     * @return static
     * @see addElapsedMinutes()
     */
    public function subElapsedMinutes(int $value): static
    {
        return $this->addElapsedMinutes(-$value);
    }

    /**
     * Add seconds to the instance using elapsed time.
     *
     * Unlike `addSeconds()` which uses wall clock time, this method
     * adds actual elapsed time by manipulating the Unix timestamp.
     * This is important when working across DST transitions where
     * wall clock time and elapsed time differ.
     *
     * @param int $value The number of seconds to add.
     * @return static
     */
    public function addElapsedSeconds(int $value): static
    {
        return $this->setTimestamp($this->getTimestamp() + $value);
    }

    /**
     * Remove seconds from the instance using elapsed time.
     *
     * @param int $value The number of seconds to remove.
     * @return static
     * @see addElapsedSeconds()
     */
    public function subElapsedSeconds(int $value): static
    {
        return $this->addElapsedSeconds(-$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(sprintf('first day of january %d, midnight', $year));
    }

    /**
     * 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(sprintf('last day of december %d, 23:59:59', $year));
    }

    /**
     * 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(sprintf('first day of january %d, midnight', $year));
    }

    /**
     * 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(sprintf('last day of december %d, 23:59:59', $year));
    }

    /**
     * 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 (use Chronos::MONDAY through
     *   Chronos::SUNDAY), or null for a sensible default.
     * @return static
     */
    public function next(?int $dayOfWeek = null): static
    {
        if ($dayOfWeek === null) {
            $dayOfWeek = $this->dayOfWeek;
        }

        $day = static::$days[$dayOfWeek];

        return $this->modify(sprintf('next %s, midnight', $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 (use Chronos::MONDAY through
     *   Chronos::SUNDAY), or null for a sensible default.
     * @return static
     */
    public function previous(?int $dayOfWeek = null): static
    {
        if ($dayOfWeek === null) {
            $dayOfWeek = $this->dayOfWeek;
        }

        $day = static::$days[$dayOfWeek];

        return $this->modify(sprintf('last %s, midnight', $day));
    }

    /**
     * Get the next occurrence of a given day of the week at a specific time.
     *
     * Unlike `next()`, this method considers both the day AND the time. If
     * today is the target day and the specified time hasn't passed yet,
     * it returns today at that time. Otherwise, it returns next week.
     *
     * This is useful when you need a relative date that always points to
     * the next future occurrence of a specific day and time.
     *
     * ### Example
     *
     * ```
     * // If it's Tuesday 9am, get "Tuesday 12pm" (today)
     * // If it's Tuesday 4pm, get "Tuesday 12pm" (next week)
     * $date = Chronos::now()->nextOccurrenceOf(Chronos::TUESDAY, 12, 0);
     * ```
     *
     * @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.)
     * @param int $hour The hour (0-23)
     * @param int $minute The minute (0-59)
     * @param int $second The second (0-59)
     * @return static
     */
    public function nextOccurrenceOf(
        int $dayOfWeek,
        int $hour,
        int $minute = 0,
        int $second = 0,
    ): static {
        // If today is the target day
        if ($this->dayOfWeek === $dayOfWeek) {
            $todayAtTime = $this->setTime($hour, $minute, $second);
            // If the time hasn't passed yet, return today
            if ($todayAtTime->greaterThan($this)) {
                return $todayAtTime;
            }
        }

        // Otherwise, get next week's occurrence
        return $this->next($dayOfWeek)->setTime($hour, $minute, $second);
    }

    /**
     * Get the previous occurrence of a given day of the week at a specific time.
     *
     * Unlike `previous()`, this method considers both the day AND the time.
     * If today is the target day and the specified time has already passed,
     * it returns today at that time. Otherwise, it returns last week.
     *
     * ### Example
     *
     * ```
     * // If it's Tuesday 4pm, get "Tuesday 12pm" (today, already passed)
     * // If it's Tuesday 9am, get "Tuesday 12pm" (last week)
     * $date = Chronos::now()->previousOccurrenceOf(Chronos::TUESDAY, 12, 0);
     * ```
     *
     * @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.)
     * @param int $hour The hour (0-23)
     * @param int $minute The minute (0-59)
     * @param int $second The second (0-59)
     * @return static
     */
    public function previousOccurrenceOf(
        int $dayOfWeek,
        int $hour,
        int $minute = 0,
        int $second = 0,
    ): static {
        // If today is the target day
        if ($this->dayOfWeek === $dayOfWeek) {
            $todayAtTime = $this->setTime($hour, $minute, $second);
            // If the time has already passed, return today
            if ($todayAtTime->lessThan($this)) {
                return $todayAtTime;
            }
        }

        // Otherwise, get last week's occurrence
        return $this->previous($dayOfWeek)->setTime($hour, $minute, $second);
    }

    /**
     * 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 (use Chronos::MONDAY through
     *   Chronos::SUNDAY), or null for a sensible default.
     * @return static
     */
    public function firstOfMonth(?int $dayOfWeek = null): static
    {
        $day = $dayOfWeek === null ? 'day' : static::$days[$dayOfWeek];

        return $this->modify(sprintf('first %s of this month, midnight', $day));
    }

    /**
     * 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 (use Chronos::MONDAY through
     *   Chronos::SUNDAY), or null for a sensible default.
     * @return static
     */
    public function lastOfMonth(?int $dayOfWeek = null): static
    {
        $day = $dayOfWeek === null ? 'day' : static::$days[$dayOfWeek];

        return $this->modify(sprintf('last %s of this month, midnight', $day));
    }

    /**
     * Get the nth occurrence of a day of the week in the current month.
     *
     * Returns false if the requested occurrence doesn't exist in the month.
     * For example, requesting the 5th Monday will return false for months
     * that only have 4 Mondays.
     *
     * Example:
     * ```
     * $date = new Chronos('2024-01-15');
     * $date->nthOfMonth(2, Chronos::TUESDAY); // 2nd Tuesday of January
     * $date->nthOfMonth(5, Chronos::MONDAY);  // false if no 5th Monday
     * ```
     *
     * @param int $nth The occurrence number (1 = first, 2 = second, etc.).
     * @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.).
     * @return static|false The date of the nth occurrence, or false if it doesn't exist.
     */
    public function nthOfMonth(int $nth, int $dayOfWeek): static|false
    {
        $dateTime = $this->firstOfMonth();
        $check = $dateTime->format('Y-m');
        $dateTime = $dateTime->modify(sprintf('+%d ', $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 (use Chronos::MONDAY through
     *   Chronos::SUNDAY), or null for a sensible default.
     * @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 (use Chronos::MONDAY through
     *   Chronos::SUNDAY), or null for a sensible default.
     * @return static
     */
    public function lastOfQuarter(?int $dayOfWeek = null): static
    {
        return $this
            ->day(1)
            ->month($this->quarter * Chronos::MONTHS_PER_QUARTER)
            ->lastOfMonth($dayOfWeek);
    }

    /**
     * Get the nth occurrence of a day of the week in the current quarter.
     *
     * Returns false if the requested occurrence doesn't exist in the quarter.
     * Quarters are: Q1 (Jan-Mar), Q2 (Apr-Jun), Q3 (Jul-Sep), Q4 (Oct-Dec).
     *
     * Example:
     * ```
     * $date = new Chronos('2024-02-15'); // Q1
     * $date->nthOfQuarter(5, Chronos::FRIDAY); // 5th Friday of Q1
     * ```
     *
     * @param int $nth The occurrence number (1 = first, 2 = second, etc.).
     * @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.).
     * @return static|false The date of the nth occurrence, or false if it doesn't exist.
     */
    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 (use Chronos::MONDAY through
     *   Chronos::SUNDAY), or null for a sensible default.
     * @return static
     */
    public function firstOfYear(?int $dayOfWeek = null): static
    {
        $day = $dayOfWeek === null ? 'day' : static::$days[$dayOfWeek];

        return $this->modify(sprintf('first %s of january, midnight', $day));
    }

    /**
     * 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 (use Chronos::MONDAY through
     *   Chronos::SUNDAY), or null for a sensible default.
     * @return static
     */
    public function lastOfYear(?int $dayOfWeek = null): static
    {
        $day = $dayOfWeek === null ? 'day' : static::$days[$dayOfWeek];

        return $this->modify(sprintf('last %s of december, midnight', $day));
    }

    /**
     * Get the nth occurrence of a day of the week in the current year.
     *
     * Returns false if the requested occurrence doesn't exist in the year
     * (e.g., requesting the 53rd Monday in a year with only 52).
     *
     * Example:
     * ```
     * $date = new Chronos('2024-06-15');
     * $date->nthOfYear(1, Chronos::MONDAY);  // First Monday of the year
     * $date->nthOfYear(52, Chronos::FRIDAY); // 52nd Friday of the year
     * ```
     *
     * @param int $nth The occurrence number (1 = first, 2 = second, etc.).
     * @param int $dayOfWeek The day of the week (use Chronos::MONDAY, etc.).
     * @return static|false
     */
    public function nthOfYear(int $nth, int $dayOfWeek): static|false
    {
        $dateTime = $this->firstOfYear()->modify(sprintf('+%d ', $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 earlier of this instance and another.
     *
     * Returns whichever datetime comes first chronologically.
     * If no other instance is provided, compares against the current time.
     *
     * @param \DateTimeInterface|null $other The instance to compare with. Defaults to now.
     * @return static The earlier of the two datetimes.
     */
    public function min(?DateTimeInterface $other = null): static
    {
        $other ??= static::now($this->tz);
        $winner = $this->lessThan($other) ? $this : $other;
        if ($winner instanceof static) {
            return $winner;
        }

        return new static($winner);
    }

    /**
     * Get the later of this instance and another.
     *
     * Returns whichever datetime comes last chronologically.
     * If no other instance is provided, compares against the current time.
     *
     * @param \DateTimeInterface|null $other The instance to compare with. Defaults to now.
     * @return static The later of the two datetimes.
     */
    public function max(?DateTimeInterface $other = null): static
    {
        $other ??= static::now($this->tz);
        $winner = $this->greaterThan($other) ? $this : $other;
        if ($winner instanceof static) {
            return $winner;
        }

        return new static($winner);
    }

    /**
     * Get the midpoint between this instance and another.
     *
     * Calculates the datetime that is exactly halfway between the current
     * instance and the given instance. If no other instance is provided,
     * uses the current time (now).
     *
     * Example:
     * ```
     * $jan1 = new Chronos('2024-01-01 00:00:00');
     * $jan3 = new Chronos('2024-01-03 00:00:00');
     * $midpoint = $jan1->average($jan3); // 2024-01-02 00:00:00
     * ```
     *
     * @param \DateTimeInterface|null $other The instance to find midpoint with. Defaults to now.
     * @return static The datetime exactly between this instance and the other.
     */
    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 callback.
     *
     * Iterates through the date range at the given interval and counts
     * how many times the callback returns true.
     *
     * Example:
     * ```
     * // Count weekdays between two dates
     * $start = new Chronos('2024-01-01');
     * $end = new Chronos('2024-01-31');
     * $weekdays = $start->diffFiltered(
     *     new DateInterval('P1D'),
     *     fn($date) => !$date->isWeekend(),
     *     $end
     * );
     * ```
     *
     * @param \DateInterval $interval An interval to traverse by (e.g., P1D for daily).
     * @param callable $callback Filter function that receives each date in the range.
     *   Should return true to count the date, false to skip it.
     *   Signature: `function(Chronos $date): bool`
     * @param \DateTimeInterface|null $other The end date. Defaults to now.
     * @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 The count of intervals where the callback returned true.
     */
    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): bool {
            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): bool {
            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);
    }

    /**
     * Converts the time zone to UTC and returns a string in RFC7231 format.
     * This replaced the deprecated and broken ``DATE_RFC7231`` formatting constant.
     *
     * @return string
     */
    public function toRfc7231String(): string
    {
        return $this->setTimezone('UTC')->format('D, d M Y H:i:s \G\M\T');
    }

    /**
     * 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());
    }

    /**
     * Returns the date and time as an associative array.
     *
     * @return array{year: int, month: int, day: int, hour: int, minute: int, second: int, microsecond: int, timezone: string}
     */
    public function toArray(): array
    {
        return [
            'year' => $this->year,
            'month' => $this->month,
            'day' => $this->day,
            'hour' => $this->hour,
            'minute' => $this->minute,
            'second' => $this->second,
            'microsecond' => $this->microsecond,
            'timezone' => $this->timezone->getName(),
        ];
    }

    /**
     * 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',
        ];

        return match (true) {
            isset($formats[$name]) => (int)$this->format($formats[$name]),
            $name === 'dayOfWeekName' => $this->format('l'),
            $name === 'weekOfMonth' => (int)ceil($this->day / Chronos::DAYS_PER_WEEK),
            $name === 'age' => $this->diffInYears(),
            $name === 'quarter' => (int)ceil($this->month / 3),
            $name === 'half' => $this->month <= 6 ? 1 : 2,
            $name === 'offset' => $this->getOffset(),
            $name === 'offsetHours' => $this->getOffset() / Chronos::SECONDS_PER_MINUTE / Chronos::MINUTES_PER_HOUR,
            $name === 'dst' => $this->format('I') === '1',
            $name === 'local' => $this->offset === $this->setTimezone(date_default_timezone_get())->offset,
            $name === 'utc' => $this->offset === 0,
            $name === 'timezone' || $name === 'tz' => $this->getTimezone(),
            $name === 'timezoneName' || $name === 'tzName' => $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) {
            return false;
        }

        return true;
    }

    /**
     * Return properties for debugging.
     *
     * @return array
     */
    public function __debugInfo(): array
    {
        $timezone = $this->getTimezone();

        return [
            'hasFixedNow' => static::hasTestNow(),
            'time' => $this->format('Y-m-d H:i:s.u'),
            'timezone' => $timezone->getName(),
        ];
    }
}
