Repository: briannesbitt/Carbon (PHP)

Issue:
Carbon::createFromTime() responds differently under Carbon::setTestNow()
<!--
    🛑 DON'T REMOVE ME.
    This issue template apply to all
      - bug reports,
      - feature proposals,
      - and documentation requests

    Having all those informations will allow us to know exactly
    what you expect and answer you faster and precisely (answer
    that matches your Carbon version, PHP version and usage).
    
    Note: Comments between <!- - and - -> won't appear in the final
    issue (See [Preview] tab).
-->
Hello,

I encountered an issue with the following code:
```php
$testTime = Carbon\Carbon::createFromTime(12, 0, 0, 'America/Anchorage');
echo $testTime->toIso8601String() . PHP_EOL;

$knownDate = Carbon\Carbon::create(2021, 10, 28, 7, 38, 0, 'UTC');
Carbon\Carbon::setTestNow($knownDate);

$testTime = Carbon\Carbon::createFromTime(12, 0, 0, 'America/Anchorage');
echo $testTime->toIso8601String() . PHP_EOL;

```

Carbon version: 2.53.1

PHP version: 8.0.12

<!--
    Run the command `composer show nesbot/carbon`
    to get "versions"
    Use `echo phpversion();`
    to get PHP version.

    Some issues can depends on your context, settings,
    macros, timezone, language. So to be sure the code
    you give is enough to reproduce your bug, try it
    first in:
    https://try-carbon.herokuapp.com/?theme=xcode&export&embed

    You can use the [Options] button to change the version
    then when you get the bug with this editor, you can use
    the [Export] button, copy the link of the opened tab,
    then paste it in the issue. Then we can immediatly get
    your issue.
-->

Notes:
- I'm currently in the UK, which at the time of this test is in BST (UTC+01:00).
- The `setTestNow()` is set to roughly the current date & time so essentially should have no real effect on the results
- I've deliberately picked "America/Anchorage" in this test as at the time of writing (8.38am BST) it is in a different "day"
- When reproducing with the code above, you should amend the call to `setTestNow()` to your current time, and the "America/Anchorage" references to a timezone that is in a different day from your current TZ

I expected to get the following, e.g. since `setTestNow()` is essentially faking the "current" time, there should be no change in the results of `createFromTime()`. 

```
2021-10-27T12:00:00-08:00
2021-10-27T12:00:00-08:00
```
<!--
    Always give your expectations. Each use has their owns.
    You may want daylight saving time to be taken into account,
    someone else want it to be ignored. You may want timezone
    to be used in comparisons, someone else may not, etc.
-->

But I actually get the following, note that when running under `setTestNow()` a different day is generated by `createFromTime()`

```
2021-10-27T12:00:00-08:00
2021-10-28T12:00:00-08:00
```
<!--
    If you did not succeed to get the same result in
    https://try-carbon.herokuapp.com/ then precise the
    result you get.
-->

Thanks!



Discussion:
I think https://github.com/briannesbitt/Carbon/blob/434e81b4edc96e0fec1fbc7cd5b81f4d294ed794/src/Carbon/Traits/Creator.php#L342 is the culprit where the fake time is called without any reference to the requested timezone?
I will inspect, here is a work-around you can use meanwhile:

```php
Carbon::today('America/Anchorage')->setTime(12, 0, 0)
```
It will be fixed in the next release **2.54.0**.
Thanks!
⚠️ Important notice about this change: your unit tests may start to fail after this version. But the behavior when the time is not mocked using `setTestNow()` is not impacted. Only the behavior when a testNow value is set has changed. And the new behavior is actually closer to the behavior with real time. So if you have failing tests, it probably means the assertion you made actually never happen with real time.

After this version, the timezone of the object you'll pass to `setTestNow()` will no longer be relevant and this is correct because in real life, `Carbon::now()->timezone` and `date_default_timezone_get()` will always be the same.

However until **2.53.1** if your default timezone is UTC and you mock the time using `Carbon::setTestNow(Carbon::parse('2021-01-25 00:00 Asia/Tokyo'))`, then `Carbon::now()` next calls returned a date instance of `2021-01-25 00:00 Asia/Tokyo` while `DateTime`, `date()`, `date_default_timezone_get()` will continue to return `UTC` dates which can lead to inconsistent behavior.

After **2.54.0**, with the same, `setTestNow()` will now only change the moment that is mocked, not the timezone. To change both you can use `Carbon::setTestNowAndTimezone()` which will basically mock the time + run `date_default_timezone_set()` but we recommend to mock them separately in your test.



Patch:
diff --git a/src/Carbon/Traits/Creator.php b/src/Carbon/Traits/Creator.php
index 8d0f98bd57..f90195a018 100644
--- a/src/Carbon/Traits/Creator.php
+++ b/src/Carbon/Traits/Creator.php
@@ -339,7 +339,7 @@ private static function createNowInstance($tz)
             return $now(static::now($tz));
         }
 
-        return $now;
+        return $now->avoidMutation()->tz($tz);
     }
 
     /**
diff --git a/src/Carbon/Traits/Date.php b/src/Carbon/Traits/Date.php
index 69a9b5ce26..fc180fe47c 100644
--- a/src/Carbon/Traits/Date.php
+++ b/src/Carbon/Traits/Date.php
@@ -1573,7 +1573,13 @@ public function tz($value = null)
     #[ReturnTypeWillChange]
     public function setTimezone($value)
     {
-        return parent::setTimezone(static::safeCreateDateTimeZone($value));
+        $tz = static::safeCreateDateTimeZone($value);
+
+        if ($tz === false && !self::isStrictModeEnabled()) {
+            $tz = new CarbonTimeZone();
+        }
+
+        return parent::setTimezone($tz);
     }
 
     /**


Test patch:
diff --git a/tests/Carbon/ComparisonTest.php b/tests/Carbon/ComparisonTest.php
index 1be669a0c1..fb0f2bf927 100644
--- a/tests/Carbon/ComparisonTest.php
+++ b/tests/Carbon/ComparisonTest.php
@@ -36,7 +36,30 @@ public function testEqualWithTimezoneTrue()
 
     public function testEqualWithTimezoneFalse()
     {
-        $this->assertFalse(Carbon::createFromDate(2000, 1, 1, 'America/Toronto')->eq(Carbon::createFromDate(2000, 1, 1, 'America/Vancouver')));
+        $timezones = ['Europe/London', 'America/Toronto', 'America/Vancouver', 'Asia/Tokyo'];
+
+        foreach ($timezones as $a) {
+            foreach ($timezones as $b) {
+                $from = Carbon::createFromDate(2000, 1, 1, $a);
+                $to = Carbon::createFromDate(2000, 1, 1, $b);
+                $diff = (float) ($from->floatDiffInHours($to, false) + Carbon::now($a)->dst - Carbon::now($b)->dst);
+
+                $this->assertTrue(\in_array($diff, $a === $b ? [0.0] : [0.0, 24.0, -24.0], true));
+            }
+        }
+
+        Carbon::setTestNow();
+
+        foreach ($timezones as $a) {
+            foreach ($timezones as $b) {
+                $from = Carbon::createFromDate(2000, 1, 1, $a);
+                $to = Carbon::createFromDate(2000, 1, 1, $b);
+                $diff = (float) ($from->floatDiffInHours($to, false) + Carbon::now($a)->dst - Carbon::now($b)->dst);
+                $diff = round($diff * 1800) / 1800; // 2-seconds precision
+
+                $this->assertTrue(\in_array($diff, $a === $b ? [0.0] : [0.0, 24.0, -24.0], true));
+            }
+        }
     }
 
     public function testNotEqualToTrue()
@@ -49,11 +72,6 @@ public function testNotEqualToFalse()
         $this->assertFalse(Carbon::createFromDate(2000, 1, 1)->ne(Carbon::createFromDate(2000, 1, 1)));
     }
 
-    public function testNotEqualWithTimezone()
-    {
-        $this->assertTrue(Carbon::createFromDate(2000, 1, 1, 'America/Toronto')->ne(Carbon::createFromDate(2000, 1, 1, 'America/Vancouver')));
-    }
-
     public function testGreaterThanTrue()
     {
         $this->assertTrue(Carbon::createFromDate(2000, 1, 1)->gt(Carbon::createFromDate(1999, 12, 31)));
diff --git a/tests/Carbon/CreateFromTimeTest.php b/tests/Carbon/CreateFromTimeTest.php
index b0ff0faca7..ee7c0284b4 100644
--- a/tests/Carbon/CreateFromTimeTest.php
+++ b/tests/Carbon/CreateFromTimeTest.php
@@ -14,6 +14,7 @@
 namespace Tests\Carbon;
 
 use Carbon\Carbon;
+use DateTimeImmutable;
 use DateTimeZone;
 use InvalidArgumentException;
 use Tests\AbstractTestCase;
@@ -47,6 +48,23 @@ public function testCreateFromTime()
         $this->assertCarbon($d, Carbon::now()->year, Carbon::now()->month, Carbon::now()->day, 23, 5, 21);
     }
 
+    public function testCreateFromTimeWithTestNow()
+    {
+        Carbon::setTestNow();
+        $testTime = Carbon::createFromTime(12, 0, 0, 'GMT-25');
+        $today = Carbon::today('GMT-25')->modify('12:00');
+        $this->assertSame('12:00 -25:00', $testTime->format('H:i e'));
+        $this->assertSame($testTime->toIso8601String(), $today->toIso8601String());
+
+        $knownDate = Carbon::instance(new DateTimeImmutable('now UTC'));
+        Carbon::setTestNow($knownDate);
+
+        $testTime = Carbon::createFromTime(12, 0, 0, 'GMT-25');
+        $today = Carbon::today('GMT-25')->modify('12:00');
+        $this->assertSame('12:00 -25:00', $testTime->format('H:i e'));
+        $this->assertSame($testTime->toIso8601String(), $today->toIso8601String());
+    }
+
     public function testCreateFromTimeGreaterThan99()
     {
         $this->expectExceptionObject(new InvalidArgumentException(
diff --git a/tests/Carbon/CreateTest.php b/tests/Carbon/CreateTest.php
index c9b62241ee..2aa6ff6864 100644
--- a/tests/Carbon/CreateTest.php
+++ b/tests/Carbon/CreateTest.php
@@ -14,6 +14,7 @@
 namespace Tests\Carbon;
 
 use Carbon\Carbon;
+use Carbon\Exceptions\InvalidTimeZoneException;
 use Carbon\Exceptions\OutOfRangeException;
 use DateTime;
 use DateTimeZone;
@@ -223,8 +224,8 @@ public function testMake()
 
     public function testCreateWithInvalidTimezoneOffset()
     {
-        $this->expectExceptionObject(new InvalidArgumentException(
-            'Unknown or bad timezone (-28236)'
+        $this->expectExceptionObject(new InvalidTimeZoneException(
+            'Absolute timezone offset cannot be greater than 100.'
         ));
 
         Carbon::createFromDate(2000, 1, 1, -28236);
diff --git a/tests/Carbon/DiffTest.php b/tests/Carbon/DiffTest.php
index 29cbdf4364..4add6b9a1a 100644
--- a/tests/Carbon/DiffTest.php
+++ b/tests/Carbon/DiffTest.php
@@ -534,6 +534,10 @@ public function testDiffInSecondsWithTimezones()
     {
         $dtOttawa = Carbon::createFromDate(2000, 1, 1, 'America/Toronto');
         $dtVancouver = Carbon::createFromDate(2000, 1, 1, 'America/Vancouver');
+        $this->assertSame(0, $dtOttawa->diffInSeconds($dtVancouver) % (24 * 3600));
+
+        $dtOttawa = Carbon::createMidnightDate(2000, 1, 1, 'America/Toronto');
+        $dtVancouver = Carbon::createMidnightDate(2000, 1, 1, 'America/Vancouver');
         $this->assertSame(3 * 60 * 60, $dtOttawa->diffInSeconds($dtVancouver));
     }
 
diff --git a/tests/Carbon/ExpressiveComparisonTest.php b/tests/Carbon/ExpressiveComparisonTest.php
index 77b169f7e4..3c22ffd69a 100644
--- a/tests/Carbon/ExpressiveComparisonTest.php
+++ b/tests/Carbon/ExpressiveComparisonTest.php
@@ -33,11 +33,6 @@ public function testEqualWithTimezoneTrue()
         $this->assertTrue(Carbon::create(2000, 1, 1, 12, 0, 0, 'America/Toronto')->equalTo(Carbon::create(2000, 1, 1, 9, 0, 0, 'America/Vancouver')));
     }
 
-    public function testEqualWithTimezoneFalse()
-    {
-        $this->assertFalse(Carbon::createFromDate(2000, 1, 1, 'America/Toronto')->equalTo(Carbon::createFromDate(2000, 1, 1, 'America/Vancouver')));
-    }
-
     public function testNotEqualToTrue()
     {
         $this->assertTrue(Carbon::createFromDate(2000, 1, 1)->notEqualTo(Carbon::createFromDate(2000, 1, 2)));
@@ -48,11 +43,6 @@ public function testNotEqualToFalse()
         $this->assertFalse(Carbon::createFromDate(2000, 1, 1)->notEqualTo(Carbon::createFromDate(2000, 1, 1)));
     }
 
-    public function testNotEqualWithTimezone()
-    {
-        $this->assertTrue(Carbon::createFromDate(2000, 1, 1, 'America/Toronto')->notEqualTo(Carbon::createFromDate(2000, 1, 1, 'America/Vancouver')));
-    }
-
     public function testGreaterThanTrue()
     {
         $this->assertTrue(Carbon::createFromDate(2000, 1, 1)->greaterThan(Carbon::createFromDate(1999, 12, 31)));
diff --git a/tests/CarbonImmutable/ComparisonTest.php b/tests/CarbonImmutable/ComparisonTest.php
index 4c5e80ea8f..631a2c9c18 100644
--- a/tests/CarbonImmutable/ComparisonTest.php
+++ b/tests/CarbonImmutable/ComparisonTest.php
@@ -34,11 +34,6 @@ public function testEqualWithTimezoneTrue()
         $this->assertTrue(Carbon::create(2000, 1, 1, 12, 0, 0, 'America/Toronto')->equalTo(Carbon::create(2000, 1, 1, 9, 0, 0, 'America/Vancouver')));
     }
 
-    public function testEqualWithTimezoneFalse()
-    {
-        $this->assertFalse(Carbon::createFromDate(2000, 1, 1, 'America/Toronto')->equalTo(Carbon::createFromDate(2000, 1, 1, 'America/Vancouver')));
-    }
-
     public function testNotEqualToTrue()
     {
         $this->assertTrue(Carbon::createFromDate(2000, 1, 1)->notEqualTo(Carbon::createFromDate(2000, 1, 2)));
@@ -49,11 +44,6 @@ public function testNotEqualToFalse()
         $this->assertFalse(Carbon::createFromDate(2000, 1, 1)->notEqualTo(Carbon::createFromDate(2000, 1, 1)));
     }
 
-    public function testNotEqualWithTimezone()
-    {
-        $this->assertTrue(Carbon::createFromDate(2000, 1, 1, 'America/Toronto')->notEqualTo(Carbon::createFromDate(2000, 1, 1, 'America/Vancouver')));
-    }
-
     public function testGreaterThanTrue()
     {
         $this->assertTrue(Carbon::createFromDate(2000, 1, 1)->greaterThan(Carbon::createFromDate(1999, 12, 31)));
diff --git a/tests/CarbonImmutable/CreateTest.php b/tests/CarbonImmutable/CreateTest.php
index 7bdc3b87ee..d3385dc01a 100644
--- a/tests/CarbonImmutable/CreateTest.php
+++ b/tests/CarbonImmutable/CreateTest.php
@@ -14,6 +14,7 @@
 namespace Tests\CarbonImmutable;
 
 use Carbon\CarbonImmutable as Carbon;
+use Carbon\Exceptions\InvalidTimeZoneException;
 use DateTime;
 use DateTimeZone;
 use InvalidArgumentException;
@@ -200,8 +201,8 @@ public function testMake()
 
     public function testCreateWithInvalidTimezoneOffset()
     {
-        $this->expectExceptionObject(new InvalidArgumentException(
-            'Unknown or bad timezone (-28236)'
+        $this->expectExceptionObject(new InvalidTimeZoneException(
+            'Absolute timezone offset cannot be greater than 100.'
         ));
 
         Carbon::createFromDate(2000, 1, 1, -28236);
diff --git a/tests/CarbonImmutable/DiffTest.php b/tests/CarbonImmutable/DiffTest.php
index c480aa5196..2629509307 100644
--- a/tests/CarbonImmutable/DiffTest.php
+++ b/tests/CarbonImmutable/DiffTest.php
@@ -502,6 +502,10 @@ public function testDiffInSecondsWithTimezones()
     {
         $dtOttawa = Carbon::createFromDate(2000, 1, 1, 'America/Toronto');
         $dtVancouver = Carbon::createFromDate(2000, 1, 1, 'America/Vancouver');
+        $this->assertSame(0, $dtOttawa->diffInSeconds($dtVancouver) % (24 * 3600));
+
+        $dtOttawa = Carbon::createMidnightDate(2000, 1, 1, 'America/Toronto');
+        $dtVancouver = Carbon::createMidnightDate(2000, 1, 1, 'America/Vancouver');
         $this->assertSame(3 * 60 * 60, $dtOttawa->diffInSeconds($dtVancouver));
     }
 
diff --git a/tests/CarbonImmutable/ExpressiveComparisonTest.php b/tests/CarbonImmutable/ExpressiveComparisonTest.php
index 7ef8149c7a..0b339d9d03 100644
--- a/tests/CarbonImmutable/ExpressiveComparisonTest.php
+++ b/tests/CarbonImmutable/ExpressiveComparisonTest.php
@@ -33,11 +33,6 @@ public function testEqualWithTimezoneTrue()
         $this->assertTrue(Carbon::create(2000, 1, 1, 12, 0, 0, 'America/Toronto')->equalTo(Carbon::create(2000, 1, 1, 9, 0, 0, 'America/Vancouver')));
     }
 
-    public function testEqualWithTimezoneFalse()
-    {
-        $this->assertFalse(Carbon::createFromDate(2000, 1, 1, 'America/Toronto')->equalTo(Carbon::createFromDate(2000, 1, 1, 'America/Vancouver')));
-    }
-
     public function testNotEqualToTrue()
     {
         $this->assertTrue(Carbon::createFromDate(2000, 1, 1)->notEqualTo(Carbon::createFromDate(2000, 1, 2)));
@@ -48,11 +43,6 @@ public function testNotEqualToFalse()
         $this->assertFalse(Carbon::createFromDate(2000, 1, 1)->notEqualTo(Carbon::createFromDate(2000, 1, 1)));
     }
 
-    public function testNotEqualWithTimezone()
-    {
-        $this->assertTrue(Carbon::createFromDate(2000, 1, 1, 'America/Toronto')->notEqualTo(Carbon::createFromDate(2000, 1, 1, 'America/Vancouver')));
-    }
-
     public function testGreaterThanTrue()
     {
         $this->assertTrue(Carbon::createFromDate(2000, 1, 1)->greaterThan(Carbon::createFromDate(1999, 12, 31)));
diff --git a/tests/CarbonTimeZone/CreateTest.php b/tests/CarbonTimeZone/CreateTest.php
index 02400485b8..4bfced7181 100644
--- a/tests/CarbonTimeZone/CreateTest.php
+++ b/tests/CarbonTimeZone/CreateTest.php
@@ -14,7 +14,7 @@
 namespace Tests\CarbonTimeZone;
 
 use Carbon\CarbonTimeZone;
-use InvalidArgumentException;
+use Carbon\Exceptions\InvalidTimeZoneException;
 use Tests\AbstractTestCase;
 use Tests\CarbonTimeZone\Fixtures\UnknownZone;
 
@@ -47,7 +47,7 @@ public function testUnknown()
 
     public function testSafeCreateDateTimeZoneWithoutStrictMode()
     {
-        $this->expectExceptionObject(new InvalidArgumentException(
+        $this->expectExceptionObject(new InvalidTimeZoneException(
             'Absolute timezone offset cannot be greater than 100.'
         ));