Repository: briannesbitt/Carbon (PHP)

Issue:
Invalid CarbonInterval->format() results for Carbon 3 (unknown).
<!--
    🛑 DON'T REMOVE ME.
    This issue template applies to all
      - bug reports,
      - feature proposals,
      - and documentation requests

    Having all those information 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:
<!--
    ⚠️ Your code below (not the code from inside Carbon library)
    Be sure it's dependency-free and enough to reproduce the issue
    when we run it on:
    https://play.phpsandbox.io/embed/nesbot/carbon
-->
```php
$past = new \DateTime('-3 Days');
$today = new \DateTime('today');
$interval = $today->diff($past);

var_dump($interval->format('%a')); // string(1) "2"

$past = new CarbonImmutable('-3 Days');
$today = new CarbonImmutable('today');
$interval = $today->diff($past);

var_dump($interval->format('%a')); // string(9) "(unknown)"
```

Carbon version: 3.0.0 to 3.3.0 (Carbon v2 returned correct values)
<!--
    ⚠️ Run the command `composer show nesbot/carbon` to get "versions"
-->

PHP version: 8.2

<!--
    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://play.phpsandbox.io/embed/nesbot/carbon?input=%3C%3Fphp%0A%0Aecho%20Carbon%3A%3Aparse('2018-06-01')-%3EsubDay()-%3Emonth%3B

    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 immediately get
    your issue.
-->


I expected to get:

```
Valid number of days: 2 in previous example.
```
<!--
    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: 

```
(unknown)
```
<!--
    If you did not succeed to get the same result in
    https://play.phpsandbox.io/embed/nesbot/carbon?input=%3C%3Fphp%0A%0Aecho%20Carbon%3A%3Aparse('2018-06-01')%3B then precise the
    result you get.
-->

Thanks!



Discussion:
Hello,

I will investigate if we can put back this support and keep the other benefits of `CarbonInterval`, for the record, in Carbon v2 `diff` was giving a `DateInterval`n in v3, it's now equivalent to the v2 `diffAsCarbonInterval` method, and this object is not directly created from the `diff` so miss the total number of days at the moment.

However you can work around this for now using:
```php
$past = new CarbonImmutable('-3 Days');
$today = new CarbonImmutable('today');
$interval = $today->diffAsDateInterval($past);
```
We have huge project that heavily relays on Carbon almost everywhere. Including twig where such workarounds are hard to achieve (like "just" putting `(int)` in front of `diffInDays`).

Upgrading to Symfony 7 and all other libraries took 1% time.
Upgrading Carbon to 3 took 99% and is still ongoing because you forces us to upgrade from 2 which has many braking changes including such trivial thing as above. Our base code already have thousands of updated lines because all things you did with `diff*` methods. You did something very dangerous from our company perspective. Changes done to C3 may create bugs in unexpected places without warning because of processing `1.23` instead of `1` or `(int) '(unknown)'` instead of `(int` '2').
This are bugs that should never occur and occur in most unpleasant way: by searching why you have all your data wrong on the database and how to fix that after 2 months of working on production. 

This is more than ridiculous that library extending DateTime is preventing us from framework upgrades or force us to invest huge amount of time to create workarounds.

And yes, above post is dictated by frustration of someone who already spent hours trying to fix consequences of your decisions at Carbon.
Carbon is provided to you for free, we never made any promise that Carbon v2 would be compatible with whatever Symfony version that would come in the future.

The reasons why `diff*` methods are changing have been explained and warned ahead as the main change for v3, I won't repeat this part. And it's anyway too late for that.

> fix consequences of your decisions at Carbon.

Please keep it respectful, people put hard work on this project and you get it for $0.

If you think it does not worth the time you have to spend on it, you're free to build you own not-ridiculous library to make your company development.

Also it's open-source, you can propose a fix for this specific issue about `->format()`.
First of all, please take my apologies. I didn't ment to disrespect anyone. I do have full respect for anyone working for community purpose and open source liblaries.

I do know reasons why diff changes was introduced. But also It is noticable that this changes are discussed by many and considered as drastic.
We, as users, put our trust in project like this and we choose such free solution to help us in our day to day task. After years of using Carbon in specific project it is really hard to change such library. Thus it is hard to get over that we was forced to take this approches:
- do not update framework and stay on C2
- upgrade framework and Carbon with all the consequences 

Am I not legitimate to criticize decisions made just because it is open source? Maybe not. But i did.

----

About the issue. I did some research before posting.

`%a` is based on `$days` which is set while using `diff` method on `\DateTime` instance. `diffAsCarbonInterval` is not using core `diff` method (and that understandable).
`\DateInterval` instance created by constructor has `$days = false`.
But you already know all of this considering this comment from `CarbonInterval::instance`
```
     * Create a CarbonInterval instance from a DateInterval one.  Can not instance
     * DateInterval objects created from DateTime::diff() as you can't externally
     * set the $days field.
 ```
Maybe someone knowing this library better than me (or smarter than me) will find better solution than overriding `format` method to use other parameter than `days` to display `%a` result.

With best regards.
> this changes are discussed by many and considered as drastic

Yes, and all those complains happened in a short period, but for years, a ton of complains about the Carbon v2 behavior of the diff methods also accumulated and for reasons. And it is healthy, on major bump to rework such pain points and reconsider what makes sense.

> Am I not legitimate to criticize decisions made

Let's step back on this as I'm not sure what decision we're talking about. If we hadn't touched the library at all during the last 2 years, you would not be able neither to use Carbon with Symfony 7, so when you say you have those 2 options:

> - do not update framework and stay on C2
> - upgrade framework and Carbon with all the consequences

By actually making the Carbon 3 version you have the second option and while we have few reports we also have a lot of download of silent users (so assuming satisfied with this option), without the "decision" taken to work on this new version, you would just be stuck with the 1 option:

> - do not update framework and stay on C2

No one removed an option from you. Using Carbon 2 with Symfony 7 just never has been possible.

We just decide not to work on this particular path.

> Am I not legitimate to criticize decisions made just because it is open source? 

With all the users we have, any decision and its opposite decision will always make some of them unhappy.

You can still complain but for the reason above and the fact we own nothing to companies using our libraries, we'll not use that as an reliable indicator of either changes are good or not for the majority of our users and so, this is likely not having any useful impact.

On the other side, you can fork Carbon v2 branch, and try to make your own way to make it compatible with Symfony 7 without picking the Carbon 3 changes.

BTW it will be way more easy for you because you won't have to keep any backward compatibility with older Symfony and PHP versions, keeping minor versions backward-compatible is providing stability to users (within a given major version) but is putting a lot of burden on maintainers.
Thank you for your time to respond.
Hope someone will find way to fix above. I do prefer to be on time with libraries thus we will take time to upgrade all places with workarounds or other methods.

With regards.
So I took the path to override `format`, `->days` will still not be usable, but `->format('%a')` will returns (once #3031 is merged and 3.3.1 released) what `DateInterval::format` would have returned as soon as the interval was created from a `diff()`.
Seeing PR im glad I didn't do it by myself. Without knowing library and its nuances my resolution wouldn't be fully correct. Thank you for fix & your support.



Patch:
diff --git a/src/Carbon/CarbonInterval.php b/src/Carbon/CarbonInterval.php
index dcfcaa721e..d6373e2432 100644
--- a/src/Carbon/CarbonInterval.php
+++ b/src/Carbon/CarbonInterval.php
@@ -289,6 +289,11 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
      */
     protected ?CarbonInterface $endDate = null;
 
+    /**
+     * End date if interval was created from a difference between 2 dates.
+     */
+    protected ?DateInterval $rawInterval = null;
+
     protected ?array $initialValues = null;
 
     /**
@@ -303,12 +308,14 @@ public function setTimezone(DateTimeZone|string|int $timezone): static
             $this->startDate = $this->startDate
                 ->avoidMutation()
                 ->setTimezone($timezone);
+            $this->rawInterval = null;
         }
 
         if ($this->endDate) {
             $this->endDate = $this->endDate
                 ->avoidMutation()
                 ->setTimezone($timezone);
+            $this->rawInterval = null;
         }
 
         return $this;
@@ -326,12 +333,14 @@ public function shiftTimezone(DateTimeZone|string|int $timezone): static
             $this->startDate = $this->startDate
                 ->avoidMutation()
                 ->shiftTimezone($timezone);
+            $this->rawInterval = null;
         }
 
         if ($this->endDate) {
             $this->endDate = $this->endDate
                 ->avoidMutation()
                 ->shiftTimezone($timezone);
+            $this->rawInterval = null;
         }
 
         return $this;
@@ -781,6 +790,7 @@ public function optimize(): static
         $this->originalInput = null;
         $this->startDate = null;
         $this->endDate = null;
+        $this->rawInterval = null;
 
         return $this;
     }
@@ -1091,8 +1101,10 @@ public static function diff($start, $end = null, bool $absolute = false, array $
     {
         $start = $start instanceof CarbonInterface ? $start : Carbon::make($start);
         $end = $end instanceof CarbonInterface ? $end : Carbon::make($end);
-        $interval = static::instance($start->diffAsDateInterval($end, $absolute), $skip);
+        $rawInterval = $start->diffAsDateInterval($end, $absolute);
+        $interval = static::instance($rawInterval, $skip);
 
+        $interval->rawInterval = $rawInterval;
         $interval->startDate = $start;
         $interval->endDate = $end;
         $interval->initialValues = $interval->getInnerValues();
@@ -2042,6 +2054,19 @@ public function forHumans($syntax = null, $short = false, $parts = self::NO_LIMI
         return $this->translate($transId, array_merge($time, $interpolations, $time), null, $translator);
     }
 
+    public function format(string $format): string
+    {
+        $output = parent::format($format);
+
+        if (!str_contains($format, '%a') || !isset($this->startDate, $this->endDate)) {
+            return $output;
+        }
+
+        $this->rawInterval ??= $this->startDate->diffAsDateInterval($this->endDate);
+
+        return str_replace('(unknown)', $this->rawInterval->format('%a'), $output);
+    }
+
     /**
      * Format the instance as a string using the forHumans() function.
      *
@@ -3269,6 +3294,7 @@ private function checkStartAndEnd(): void
         ) {
             $this->startDate = null;
             $this->endDate = null;
+            $this->rawInterval = null;
         }
     }
 }


Test patch:
diff --git a/tests/Carbon/DiffTest.php b/tests/Carbon/DiffTest.php
index 00d49b760a..3a4893fe67 100644
--- a/tests/Carbon/DiffTest.php
+++ b/tests/Carbon/DiffTest.php
@@ -2169,4 +2169,13 @@ public function testDiffWithTimezones()
                 ->diffInDays(Carbon::parse('2024-04-01', 'America/New_York')),
         );
     }
+
+    public function testAFormat()
+    {
+        $past = new Carbon('-3 Days');
+        $today = new Carbon('today');
+        $interval = $today->diff($past);
+
+        $this->assertSame('2', $interval->format('%a'));
+    }
 }
diff --git a/tests/CarbonImmutable/DiffTest.php b/tests/CarbonImmutable/DiffTest.php
index f91aeef9b5..b8749c655b 100644
--- a/tests/CarbonImmutable/DiffTest.php
+++ b/tests/CarbonImmutable/DiffTest.php
@@ -1935,4 +1935,13 @@ public function testDaysDiffPreservation()
             'minimumUnit' => 'd',
         ]));
     }
+
+    public function testAFormat()
+    {
+        $past = new Carbon('-3 Days');
+        $today = new Carbon('today');
+        $interval = $today->diff($past);
+
+        $this->assertSame('2', $interval->format('%a'));
+    }
 }