diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 0660724f5019..172860f31e08 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -144,7 +144,7 @@ public static function parse(string $datetime, $timezone = null, ?string $locale */ public static function today($timezone = null, ?string $locale = null) { - return new static(date('Y-m-d 00:00:00'), $timezone, $locale); + return static::now($timezone, $locale)->setTime(0, 0, 0, 0); } /** @@ -158,7 +158,7 @@ public static function today($timezone = null, ?string $locale = null) */ public static function yesterday($timezone = null, ?string $locale = null) { - return new static(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale); + return static::now($timezone, $locale)->modify('-1 day')->setTime(0, 0, 0, 0); } /** @@ -172,7 +172,7 @@ public static function yesterday($timezone = null, ?string $locale = null) */ public static function tomorrow($timezone = null, ?string $locale = null) { - return new static(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale); + return static::now($timezone, $locale)->modify('+1 day')->setTime(0, 0, 0, 0); } /** diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index ef6712f37259..4eed1738e548 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -190,6 +190,57 @@ public function testTomorrow(): void $this->assertSame(date('Y-m-d 00:00:00', strtotime('+1 day')), $time->toDateTimeString()); } + public function testTodayWithTimezoneAcrossDateBoundary(): void + { + // When UTC is 2026-09-07 23:30:00, in Asia/Tokyo (+09:00) it is already 2026-09-08 08:30:00 + Time::setTestNow('2026-09-07 23:30:00', 'UTC'); + + $tokyoToday = Time::today('Asia/Tokyo'); + $this->assertSame('2026-09-08 00:00:00', $tokyoToday->toDateTimeString()); + + // When UTC is 2026-09-08 02:00:00, in America/New_York (-04:00 EDT) it is still 2026-09-07 22:00:00 + Time::setTestNow('2026-09-08 02:00:00', 'UTC'); + + $nyToday = Time::today('America/New_York'); + $this->assertSame('2026-09-07 00:00:00', $nyToday->toDateTimeString()); + + Time::setTestNow(); + } + + public function testYesterdayWithTimezoneAcrossDateBoundary(): void + { + // When UTC is 2026-09-07 23:30:00, in Tokyo it is 2026-09-08, so Tokyo yesterday is 2026-09-07 + Time::setTestNow('2026-09-07 23:30:00', 'UTC'); + + $tokyoYesterday = Time::yesterday('Asia/Tokyo'); + $this->assertSame('2026-09-07 00:00:00', $tokyoYesterday->toDateTimeString()); + + // When UTC is 2026-09-08 02:00:00, in NY it is 2026-09-07, so NY yesterday is 2026-09-06 + Time::setTestNow('2026-09-08 02:00:00', 'UTC'); + + $nyYesterday = Time::yesterday('America/New_York'); + $this->assertSame('2026-09-06 00:00:00', $nyYesterday->toDateTimeString()); + + Time::setTestNow(); + } + + public function testTomorrowWithTimezoneAcrossDateBoundary(): void + { + // When UTC is 2026-09-07 23:30:00, in Tokyo it is 2026-09-08, so Tokyo tomorrow is 2026-09-09 + Time::setTestNow('2026-09-07 23:30:00', 'UTC'); + + $tokyoTomorrow = Time::tomorrow('Asia/Tokyo'); + $this->assertSame('2026-09-09 00:00:00', $tokyoTomorrow->toDateTimeString()); + + // When UTC is 2026-09-08 02:00:00, in NY it is 2026-09-07, so NY tomorrow is 2026-09-08 + Time::setTestNow('2026-09-08 02:00:00', 'UTC'); + + $nyTomorrow = Time::tomorrow('America/New_York'); + $this->assertSame('2026-09-08 00:00:00', $nyTomorrow->toDateTimeString()); + + Time::setTestNow(); + } + public function testCreateFromDate(): void { $time = Time::createFromDate(2017, 03, 05, 'America/Chicago'); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index f13a3e4b2b49..af5ef0c38677 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -46,6 +46,7 @@ Bugs Fixed - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). +- **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. - **Cache:** Fixed ``MemcachedHandler::decrement()`` initializing a non-existent counter to the positive offset. Missing counters are now initialized to ``0``, reflecting Memcached's unsigned, saturating counter semantics.