From 8480b79c035e48975aa864f57f6957967a25f43e Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Sat, 25 Apr 2026 18:51:29 +0530 Subject: [PATCH 1/4] feat: Add `clamp()` method in TimeTrait with docs and tests Co-authored-by: Copilot --- system/I18n/Exceptions/I18nException.php | 10 +++++ system/I18n/TimeTrait.php | 26 +++++++++++++ system/Language/en/Time.php | 39 ++++++++++---------- tests/system/I18n/TimeTest.php | 29 +++++++++++++++ user_guide_src/source/changelogs/v4.8.0.rst | 1 + user_guide_src/source/libraries/time.rst | 14 +++++++ user_guide_src/source/libraries/time/045.php | 14 +++++++ 7 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 user_guide_src/source/libraries/time/045.php 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..cbe23f463073 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -862,6 +862,32 @@ 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 + { + $start = $start instanceof DateTimeInterface ? $start : new static($start, $this->timezone, $this->locale); + $end = $end instanceof DateTimeInterface ? $end : new static($end, $this->timezone, $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..aa5e4f557e4b 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($min, $max)`` method which returns clamped time between two times. See :ref:`TimeTrait::clamp()`. *************** 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..c574fb035c3c --- /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 minimum time, it will return new instance of minimum 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 maximum time, it will return new instance of maximum 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 From 12218483341dbe3e48745518fc1b14d66630592d Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Sat, 25 Apr 2026 18:57:35 +0530 Subject: [PATCH 2/4] docs: Updated changelog for proper link and consistent signature --- user_guide_src/source/changelogs/v4.8.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index aa5e4f557e4b..ba86fae9ddf6 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -263,7 +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($min, $max)`` method which returns clamped time between two times. See :ref:`TimeTrait::clamp()`. +- Added new ``TimeTrait::clamp($start, $end)`` method which returns clamped time between two times. See :ref:`time-setters-clamp` for details. *************** Message Changes From b70871e4ca2f572cb9a5a31ee2e7f6bd1da358f4 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Sat, 25 Apr 2026 18:58:34 +0530 Subject: [PATCH 3/4] docs: Update some wording Co-authored-by: Copilot --- user_guide_src/source/libraries/time/045.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/time/045.php b/user_guide_src/source/libraries/time/045.php index c574fb035c3c..f007e5c8eec7 100644 --- a/user_guide_src/source/libraries/time/045.php +++ b/user_guide_src/source/libraries/time/045.php @@ -7,8 +7,8 @@ // If the time is between the two times, it will return the time itself. echo $time->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 minimum time, it will return new instance of minimum time. +// 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 maximum time, it will return new instance of maximum time. +// 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 From 22b0df6799934330d9064f03ae6e0d79eb8b43ac Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Sat, 25 Apr 2026 19:17:00 +0530 Subject: [PATCH 4/4] fix: Some PHPStan errors --- system/I18n/TimeTrait.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index cbe23f463073..4cf4b7847572 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -870,8 +870,17 @@ public function subYears(int $years) */ public function clamp(DateTimeInterface|self|string $start, DateTimeInterface|self|string $end): static { - $start = $start instanceof DateTimeInterface ? $start : new static($start, $this->timezone, $this->locale); - $end = $end instanceof DateTimeInterface ? $end : new static($end, $this->timezone, $this->locale); + 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());