diff --git a/system/I18n/Exceptions/I18nException.php b/system/I18n/Exceptions/I18nException.php index ec9dd3277e1e..6b693b98fd41 100644 --- a/system/I18n/Exceptions/I18nException.php +++ b/system/I18n/Exceptions/I18nException.php @@ -96,4 +96,14 @@ public static function forInvalidSeconds(string $seconds) { return new static(lang('Time.invalidSeconds', [$seconds])); } + + /** + * Thrown when the supplied clamp range is invalid. + * + * @return static + */ + public static function forInvalidClampRange(string $start, string $end) + { + return new static(lang('Time.invalidClampRange', [$start, $end])); + } } diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 5b2c7cff1371..4cf4b7847572 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -862,6 +862,41 @@ public function subYears(int $years) return $time->sub(DateInterval::createFromDateString("{$years} years")); } + /** + * Returns a new Time instance with the time clamped between the two provided times. + * If the current instance is before the $start time, a new instance will be returned with the same time as $start. + * If the current instance is after the $end time, a new instance will be returned with the same time as $end. + * Otherwise, the current instance will be returned. + */ + public function clamp(DateTimeInterface|self|string $start, DateTimeInterface|self|string $end): static + { + if ($start instanceof DateTimeInterface && ! $start instanceof self) { + $start = static::createFromInstance($start, $this->locale); + } elseif (is_string($start)) { + $start = new static($start, $this->getTimezone(), $this->locale); + } + + if ($end instanceof DateTimeInterface && ! $end instanceof self) { + $end = static::createFromInstance($end, $this->locale); + } elseif (is_string($end)) { + $end = new static($end, $this->getTimezone(), $this->locale); + } + + if ($end->isBefore($start)) { + throw I18nException::forInvalidClampRange($start->toDateTimeString(), $end->toDateTimeString()); + } + + if ($this->isBefore($start)) { + return static::createFromInstance($start, $this->locale); + } + + if ($this->isAfter($end)) { + return static::createFromInstance($end, $this->locale); + } + + return $this; + } + // -------------------------------------------------------------------- // Formatters // -------------------------------------------------------------------- diff --git a/system/Language/en/Time.php b/system/Language/en/Time.php index 2cb25218a1db..5cec64de1d4b 100644 --- a/system/Language/en/Time.php +++ b/system/Language/en/Time.php @@ -13,23 +13,24 @@ // Time language settings return [ - 'invalidFormat' => '"{0}" is not a valid datetime format', - 'invalidMonth' => 'Months must be between 1 and 12. Given: {0}', - 'invalidDay' => 'Days must be between 1 and 31. Given: {0}', - 'invalidOverDay' => 'Days must be between 1 and {0}. Given: {1}', - 'invalidHours' => 'Hours must be between 0 and 23. Given: {0}', - 'invalidMinutes' => 'Minutes must be between 0 and 59. Given: {0}', - 'invalidSeconds' => 'Seconds must be between 0 and 59. Given: {0}', - 'years' => '{0, plural, =1{# year} other{# years}}', - 'months' => '{0, plural, =1{# month} other{# months}}', - 'weeks' => '{0, plural, =1{# week} other{# weeks}}', - 'days' => '{0, plural, =1{# day} other{# days}}', - 'hours' => '{0, plural, =1{# hour} other{# hours}}', - 'minutes' => '{0, plural, =1{# minute} other{# minutes}}', - 'seconds' => '{0, plural, =1{# second} other{# seconds}}', - 'ago' => '{0} ago', - 'inFuture' => 'in {0}', - 'yesterday' => 'Yesterday', - 'tomorrow' => 'Tomorrow', - 'now' => 'Just now', + 'invalidFormat' => '"{0}" is not a valid datetime format', + 'invalidMonth' => 'Months must be between 1 and 12. Given: {0}', + 'invalidDay' => 'Days must be between 1 and 31. Given: {0}', + 'invalidOverDay' => 'Days must be between 1 and {0}. Given: {1}', + 'invalidHours' => 'Hours must be between 0 and 23. Given: {0}', + 'invalidMinutes' => 'Minutes must be between 0 and 59. Given: {0}', + 'invalidSeconds' => 'Seconds must be between 0 and 59. Given: {0}', + 'invalidClampRange' => 'The start time "{0}" must be earlier than or equal to the end time "{1}".', + 'years' => '{0, plural, =1{# year} other{# years}}', + 'months' => '{0, plural, =1{# month} other{# months}}', + 'weeks' => '{0, plural, =1{# week} other{# weeks}}', + 'days' => '{0, plural, =1{# day} other{# days}}', + 'hours' => '{0, plural, =1{# hour} other{# hours}}', + 'minutes' => '{0, plural, =1{# minute} other{# minutes}}', + 'seconds' => '{0, plural, =1{# second} other{# seconds}}', + 'ago' => '{0} ago', + 'inFuture' => 'in {0}', + 'yesterday' => 'Yesterday', + 'tomorrow' => 'Tomorrow', + 'now' => 'Just now', ]; diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 9fb4b9226a85..72170155a5d6 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -744,6 +744,35 @@ public function testSetTimestampDateTimeImmutable(): void $this->assertSame('2017-03-31 19:00:00 -05:00', $time2->format('Y-m-d H:i:s P')); } + public function testClamp(): void + { + $time = Time::parse('May 10, 2017', 'America/Chicago'); + $time2 = $time->clamp('2017-05-01', '2017-05-31'); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertSame($time, $time2); + + $time3 = $time->clamp('2017-05-11', '2017-05-31'); + + $this->assertInstanceOf(Time::class, $time3); + $this->assertNotSame($time, $time3); + $this->assertSame('2017-05-11 00:00:00', $time3->toDateTimeString()); + + $time4 = $time->clamp('2017-05-01', '2017-05-09'); + + $this->assertInstanceOf(Time::class, $time4); + $this->assertNotSame($time, $time4); + $this->assertSame('2017-05-09 00:00:00', $time4->toDateTimeString()); + } + + public function testClampException(): void + { + $this->expectException(I18nException::class); + + $time = Time::parse('May 10, 2017', 'America/Chicago'); + $time->clamp('2017-05-31', '2017-05-01'); + } + public function testToDateString(): void { $time = Time::parse('May 10, 2017', 'America/Chicago'); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index a9a33ee50165..ba86fae9ddf6 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -263,6 +263,7 @@ Others - **Float and Double Casting:** Added support for precision and rounding mode when casting to float or double in entities. - Float and Double casting now throws ``CastException::forInvalidFloatRoundingMode()`` if an rounding mode other than up, down, even or odd is provided. +- Added new ``TimeTrait::clamp($start, $end)`` method which returns clamped time between two times. See :ref:`time-setters-clamp` for details. *************** Message Changes diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index d9df6f545c51..cb0746f1f30b 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -317,6 +317,20 @@ Returns a new instance with the date set to the new timestamp: .. note:: Prior to v4.6.0, due to a bug, this method might return incorrect date/time. See :ref:`Upgrading Guide ` for details. +.. _time-setters-clamp: + +clamp() +------- + +.. versionadded:: 4.8.0 + +Returns a new Time instance with the time clamped between the two times passed in. +If the current instance is before the $min time, new instance with $min time will be returned. +If the current instance is after the $max time, new instance with $max time will be returned. +Otherwise, the current instance will be returned. + +.. literalinclude:: time/045.php + Modifying the Value =================== diff --git a/user_guide_src/source/libraries/time/045.php b/user_guide_src/source/libraries/time/045.php new file mode 100644 index 000000000000..f007e5c8eec7 --- /dev/null +++ b/user_guide_src/source/libraries/time/045.php @@ -0,0 +1,14 @@ +clamp('April 25, 2026 5:00:00pm UTC', 'April 25, 2026 7:00:00pm UTC')->toDateTimeString(); // 2026-04-25 17:46:00 + +// If the time is before the start time, it will return new instance of start time. +echo $time->clamp('April 25, 2026 6:05:00pm UTC', 'April 25, 2026 9:20:00pm UTC')->toDateTimeString(); // 2026-04-25 18:05:00 + +// If the time is after the end time, it will return new instance of end time. +echo $time->clamp('April 25, 2026 3:40:00pm UTC', 'April 25, 2026 5:00:00pm UTC')->toDateTimeString(); // 2026-04-25 17:00:00