Repository: briannesbitt/Carbon (PHP)

Issue:
diffForHumans gives wrong amount of days
Hello,

I encountered an issue with the following code:
```php
$deletedDate = Carbon::now()->startOfDay()->addDays(31);

echo $deletedDate->diffForHumans(Carbon::now()->startOfDay(), [
    'syntax' => CarbonInterface::DIFF_ABSOLUTE,
    'skip' => ['m', 'w'],
    'minimumUnit' => 'd'
]);;
```

Carbon version: 2.61.0

PHP version: 8.1.8

I expected to get:

```
31 days
```

But I actually get:

```
28 days
```

Thanks!



Discussion:
Thanks for the report, indeed when month and year of the interval are either 0 or skipped, it makes sense to use the total number of days instead of the `DateInterval` representation which loose the context.

It will be fixed in 2.64.0, you can test the fix with `composer require "nesbot/carbon:dev-master as 2.64.0"`



Patch:
diff --git a/phpstan.neon b/phpstan.neon
index 85a6635105..e6a28c05e4 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -28,9 +28,11 @@ parameters:
         - '*/tests/Carbon/Fixtures/DumpCarbon.php'
         - '*/tests/Carbon/LocalizationTest.php'
         - '*/tests/Carbon/SerializationTest.php'
+        - '*/tests/Carbon/LastErrorTest.php'
         - '*/tests/CarbonImmutable/LocalizationTest.php'
         - '*/tests/CarbonImmutable/SetStateTest.php'
         - '*/tests/CarbonImmutable/SerializationTest.php'
+        - '*/tests/CarbonImmutable/LastErrorTest.php'
         - '*/tests/Laravel/*.php'
         - '*/tests/Cli/*.php'
         - '*/tests/CarbonPeriod/Fixtures/filters.php'
diff --git a/src/Carbon/CarbonInterval.php b/src/Carbon/CarbonInterval.php
index 0589ef9452..b5e95c510d 100644
--- a/src/Carbon/CarbonInterval.php
+++ b/src/Carbon/CarbonInterval.php
@@ -887,7 +887,7 @@ public static function parseFromLocale($interval, $locale = null)
         return static::fromString(Carbon::translateTimeString($interval, $locale ?: static::getLocale(), 'en'));
     }
 
-    private static function castIntervalToClass(DateInterval $interval, string $className)
+    private static function castIntervalToClass(DateInterval $interval, string $className, array $skip = [])
     {
         $mainClass = DateInterval::class;
 
@@ -896,7 +896,7 @@ private static function castIntervalToClass(DateInterval $interval, string $clas
         }
 
         $microseconds = $interval->f;
-        $instance = new $className(static::getDateIntervalSpec($interval));
+        $instance = new $className(static::getDateIntervalSpec($interval, false, $skip));
 
         if ($microseconds) {
             $instance->f = $microseconds;
@@ -948,9 +948,9 @@ public function cast(string $className)
      *
      * @return static
      */
-    public static function instance(DateInterval $interval)
+    public static function instance(DateInterval $interval, array $skip = [])
     {
-        return self::castIntervalToClass($interval, static::class);
+        return self::castIntervalToClass($interval, static::class, $skip);
     }
 
     /**
@@ -1415,9 +1415,9 @@ protected function getForHumansParameters($syntax = null, $short = false, $parts
         $minimumUnit = 's';
         $skip = [];
         extract($this->getForHumansInitialVariables($syntax, $short));
-        $skip = array_filter((array) $skip, static function ($value) {
+        $skip = array_map('strtolower', array_filter((array) $skip, static function ($value) {
             return \is_string($value) && $value !== '';
-        });
+        }));
 
         if ($syntax === null) {
             $syntax = CarbonInterface::DIFF_ABSOLUTE;
@@ -2155,7 +2155,7 @@ public function divide($divider)
      *
      * @return string
      */
-    public static function getDateIntervalSpec(DateInterval $interval, bool $microseconds = false)
+    public static function getDateIntervalSpec(DateInterval $interval, bool $microseconds = false, array $skip = [])
     {
         $date = array_filter([
             static::PERIOD_YEARS => abs($interval->y),
@@ -2163,6 +2163,16 @@ public static function getDateIntervalSpec(DateInterval $interval, bool $microse
             static::PERIOD_DAYS => abs($interval->d),
         ]);
 
+        if (
+            $interval->days >= CarbonInterface::DAYS_PER_WEEK * CarbonInterface::WEEKS_PER_MONTH &&
+            (!isset($date[static::PERIOD_YEARS]) || \count(array_intersect(['y', 'year', 'years'], $skip))) &&
+            (!isset($date[static::PERIOD_MONTHS]) || \count(array_intersect(['m', 'month', 'months'], $skip)))
+        ) {
+            $date = [
+                static::PERIOD_DAYS => abs($interval->days),
+            ];
+        }
+
         $seconds = abs($interval->s);
         if ($microseconds && $interval->f > 0) {
             $seconds = sprintf('%d.%06d', $seconds, abs($interval->f) * 1000000);
diff --git a/src/Carbon/Traits/Difference.php b/src/Carbon/Traits/Difference.php
index eea6caa6b2..4f1fceca2d 100644
--- a/src/Carbon/Traits/Difference.php
+++ b/src/Carbon/Traits/Difference.php
@@ -83,9 +83,9 @@ protected static function fixNegativeMicroseconds(CarbonInterval $diff)
      *
      * @return CarbonInterval
      */
-    protected static function fixDiffInterval(DateInterval $diff, $absolute)
+    protected static function fixDiffInterval(DateInterval $diff, $absolute, array $skip = [])
     {
-        $diff = CarbonInterval::instance($diff);
+        $diff = CarbonInterval::instance($diff, $skip);
 
         // Work-around for https://bugs.php.net/bug.php?id=77145
         // @codeCoverageIgnoreStart
@@ -148,9 +148,9 @@ public function diff($date = null, $absolute = false)
      *
      * @return CarbonInterval
      */
-    public function diffAsCarbonInterval($date = null, $absolute = true)
+    public function diffAsCarbonInterval($date = null, $absolute = true, array $skip = [])
     {
-        return static::fixDiffInterval($this->diff($this->resolveCarbon($date), $absolute), $absolute);
+        return static::fixDiffInterval($this->diff($this->resolveCarbon($date), $absolute), $absolute, $skip);
     }
 
     /**
@@ -842,8 +842,9 @@ public function diffForHumans($other = null, $syntax = null, $short = false, $pa
         $intSyntax = $intSyntax === static::DIFF_RELATIVE_AUTO && $other === null ? static::DIFF_RELATIVE_TO_NOW : $intSyntax;
 
         $parts = min(7, max(1, (int) $parts));
+        $skip = \is_array($syntax) ? ($syntax['skip'] ?? []) : [];
 
-        return $this->diffAsCarbonInterval($other, false)
+        return $this->diffAsCarbonInterval($other, false, $skip)
             ->setLocalTranslator($this->getLocalTranslator())
             ->forHumans($syntax, (bool) $short, $parts, $options ?? $this->localHumanDiffOptions ?? static::getHumanDiffOptions());
     }


Test patch:
diff --git a/tests/Carbon/DiffTest.php b/tests/Carbon/DiffTest.php
index 37eae366d3..794872db1c 100644
--- a/tests/Carbon/DiffTest.php
+++ b/tests/Carbon/DiffTest.php
@@ -597,12 +597,12 @@ public function testDiffWithSkippedUnits()
         Carbon::setTestNow('2021-11-04 15:42');
 
         $this->assertSame(
-            '26 weeks from now',
+            '28 weeks from now',
             Carbon::parse('2022-05-25')
                 ->diffForHumans(['skip' => ['y', 'm']])
         );
         $this->assertSame(
-            '188 days from now',
+            '201 days from now',
             Carbon::parse('2022-05-25')
                 ->diffForHumans(['skip' => ['y', 'm', 'w']])
         );
@@ -617,7 +617,7 @@ public function testDiffWithSkippedUnits()
                 ->diffForHumans(['skip' => ['y', 'm', 'w']])
         );
         $this->assertSame(
-            '486 days ago',
+            '528 days ago',
             Carbon::parse('2020-05-25')
                 ->diffForHumans(['skip' => ['y', 'm', 'w']])
         );
@@ -1000,21 +1000,31 @@ public function testDiffForHumansOverWeekWithDefaultPartsCount()
     public function testDiffForHumansOverWeekWithPartsCount1()
     {
         $this->wrapWithTestNow(function () {
-            $this->assertSame('1 week ago', Carbon::now()->subDays(8)->diffForHumans(null, false, false, 1));
+            $this->assertSame(
+                '1 week ago',
+                Carbon::now()->subDays(8)->diffForHumans(null, false, false, 1)
+            );
         });
     }
 
     public function testDiffForHumansOverWeekWithPartsCount2()
     {
         $this->wrapWithTestNow(function () {
-            $this->assertSame('1 week 1 day ago', Carbon::now()->subDays(8)->diffForHumans(null, false, false, 2));
+            $this->assertSame(
+                '1 week 1 day ago',
+                Carbon::now()->subDays(8)->diffForHumans(null, false, false, 2)
+            );
         });
     }
 
     public function testDiffForHumansOverWeekWithMicrosecondsBuggyGap()
     {
         $this->wrapWithTestNow(function () {
-            $this->assertSame('23 hours 59 minutes 59 seconds after', Carbon::parse('2018-12-03 12:34:45.123456')->diffForHumans('2018-12-02 12:34:45.123476', ['parts' => 3]));
+            $this->assertSame(
+                '23 hours 59 minutes 59 seconds after',
+                Carbon::parse('2018-12-03 12:34:45.123456')
+                    ->diffForHumans('2018-12-02 12:34:45.123476', ['parts' => 3])
+            );
         });
     }
 
@@ -1835,4 +1845,27 @@ public function testExactMonthDiffInSeconds()
 
         $this->assertSame(2678400, $d2->diffInSeconds($d1));
     }
+
+    public function testDaysDiffPreservation()
+    {
+        $deletedDate = Carbon::now()->startOfDay()->addDays(31);
+
+        $this->assertSame('31 days', $deletedDate->diffForHumans(Carbon::now()->startOfDay(), [
+            'syntax' => CarbonInterface::DIFF_ABSOLUTE,
+            'skip' => ['m', 'w'],
+            'minimumUnit' => 'd',
+        ]));
+
+        $this->assertSame('31 days', $deletedDate->diffForHumans(Carbon::now()->startOfDay()->subHours(5), [
+            'syntax' => CarbonInterface::DIFF_ABSOLUTE,
+            'skip' => ['m', 'w'],
+            'minimumUnit' => 'd',
+        ]));
+
+        $this->assertSame('30 days', $deletedDate->diffForHumans(Carbon::now()->startOfDay()->addHours(5), [
+            'syntax' => CarbonInterface::DIFF_ABSOLUTE,
+            'skip' => ['m', 'w'],
+            'minimumUnit' => 'd',
+        ]));
+    }
 }
diff --git a/tests/Carbon/LastErrorTest.php b/tests/Carbon/LastErrorTest.php
index 0de848ae1e..3d38414dbe 100644
--- a/tests/Carbon/LastErrorTest.php
+++ b/tests/Carbon/LastErrorTest.php
@@ -14,6 +14,7 @@
 namespace Tests\Carbon;
 
 use Carbon\Carbon;
+use Carbon\Traits\Creator;
 use DateTime;
 use Tests\AbstractTestCase;
 
@@ -60,4 +61,29 @@ public function testCreateHandlesLastErrors()
 
         $this->assertSame($this->noErrors, $carbon->getLastErrors());
     }
+
+    public function testLastErrorsInitialization()
+    {
+        $obj = new class() {
+            use Creator;
+
+            /** @phpstan-ignore-next-line */
+            public function __construct($time = null, $tz = null)
+            {
+            }
+
+            public function triggerError()
+            {
+                self::setLastErrors(false);
+            }
+        };
+        $this->assertNull($obj::getLastErrors());
+        $obj->triggerError();
+        $this->assertSame([
+            'warning_count' => 0,
+            'warnings' => [],
+            'error_count' => 0,
+            'errors' => [],
+        ], $obj::getLastErrors());
+    }
 }
diff --git a/tests/CarbonImmutable/DiffTest.php b/tests/CarbonImmutable/DiffTest.php
index 8bb044bf79..517afa3744 100644
--- a/tests/CarbonImmutable/DiffTest.php
+++ b/tests/CarbonImmutable/DiffTest.php
@@ -902,21 +902,31 @@ public function testDiffForHumansOverWeekWithDefaultPartsCount()
     public function testDiffForHumansOverWeekWithPartsCount1()
     {
         $this->wrapWithTestNow(function () {
-            $this->assertSame('1 week ago', Carbon::now()->subDays(8)->diffForHumans(null, false, false, 1));
+            $this->assertSame(
+                '1 week ago',
+                Carbon::now()->subDays(8)->diffForHumans(null, false, false, 1)
+            );
         });
     }
 
     public function testDiffForHumansOverWeekWithPartsCount2()
     {
         $this->wrapWithTestNow(function () {
-            $this->assertSame('1 week 1 day ago', Carbon::now()->subDays(8)->diffForHumans(null, false, false, 2));
+            $this->assertSame(
+                '1 week 1 day ago',
+                Carbon::now()->subDays(8)->diffForHumans(null, false, false, 2)
+            );
         });
     }
 
     public function testDiffForHumansOverWeekWithMicrosecondsBuggyGap()
     {
         $this->wrapWithTestNow(function () {
-            $this->assertSame('23 hours 59 minutes 59 seconds after', Carbon::parse('2018-12-03 12:34:45.123456')->diffForHumans('2018-12-02 12:34:45.123476', ['parts' => 3]));
+            $this->assertSame(
+                '23 hours 59 minutes 59 seconds after',
+                Carbon::parse('2018-12-03 12:34:45.123456')
+                    ->diffForHumans('2018-12-02 12:34:45.123476', ['parts' => 3])
+            );
         });
     }
 
@@ -1723,4 +1733,27 @@ public function testExactMonthDiffInSeconds()
 
         $this->assertSame(2678400, $d2->diffInSeconds($d1));
     }
+
+    public function testDaysDiffPreservation()
+    {
+        $deletedDate = Carbon::now()->startOfDay()->addDays(31);
+
+        $this->assertSame('31 days', $deletedDate->diffForHumans(Carbon::now()->startOfDay(), [
+            'syntax' => CarbonInterface::DIFF_ABSOLUTE,
+            'skip' => ['m', 'w'],
+            'minimumUnit' => 'd',
+        ]));
+
+        $this->assertSame('31 days', $deletedDate->diffForHumans(Carbon::now()->startOfDay()->subHours(5), [
+            'syntax' => CarbonInterface::DIFF_ABSOLUTE,
+            'skip' => ['m', 'w'],
+            'minimumUnit' => 'd',
+        ]));
+
+        $this->assertSame('30 days', $deletedDate->diffForHumans(Carbon::now()->startOfDay()->addHours(5), [
+            'syntax' => CarbonInterface::DIFF_ABSOLUTE,
+            'skip' => ['m', 'w'],
+            'minimumUnit' => 'd',
+        ]));
+    }
 }
diff --git a/tests/CarbonImmutable/LastErrorTest.php b/tests/CarbonImmutable/LastErrorTest.php
index 6207740c3e..d35c90e823 100644
--- a/tests/CarbonImmutable/LastErrorTest.php
+++ b/tests/CarbonImmutable/LastErrorTest.php
@@ -14,6 +14,7 @@
 namespace Tests\CarbonImmutable;
 
 use Carbon\CarbonImmutable as Carbon;
+use Carbon\Traits\Creator;
 use DateTime;
 use Tests\AbstractTestCase;
 
@@ -60,4 +61,29 @@ public function testCreateHandlesLastErrors()
 
         $this->assertSame($this->noErrors, $carbon->getLastErrors());
     }
+
+    public function testLastErrorsInitialization()
+    {
+        $obj = new class() {
+            use Creator;
+
+            /** @phpstan-ignore-next-line */
+            public function __construct($time = null, $tz = null)
+            {
+            }
+
+            public function triggerError()
+            {
+                self::setLastErrors(false);
+            }
+        };
+        $this->assertNull($obj::getLastErrors());
+        $obj->triggerError();
+        $this->assertSame([
+            'warning_count' => 0,
+            'warnings' => [],
+            'error_count' => 0,
+            'errors' => [],
+        ], $obj::getLastErrors());
+    }
 }