Skip to content

Commit

Permalink
Merge pull request #2839 from briannesbitt/feature/microseconds-perio…
Browse files Browse the repository at this point in the history
…d-interval

Handle decimal part from interval unit setters
  • Loading branch information
kylekatarnls authored Sep 7, 2023
2 parents 0099ea8 + 6dbaba3 commit d3298b3
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 11 deletions.
115 changes: 105 additions & 10 deletions src/Carbon/CarbonInterval.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use InvalidArgumentException;
use ReflectionException;
use ReturnTypeWillChange;
use RuntimeException;
use Throwable;

/**
Expand Down Expand Up @@ -248,6 +249,11 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
*/
private static $flipCascadeFactors;

/**
* @var bool
*/
private static $floatSettersEnabled = false;

/**
* The registered macros.
*
Expand Down Expand Up @@ -349,6 +355,19 @@ public static function setCascadeFactors(array $cascadeFactors)
static::$cascadeFactors = $cascadeFactors;
}

/**
* This option allow you to opt-in for the Carbon 3 behavior where float
* values will no longer be cast to integer (so truncated).
*
* ⚠️ This settings will be applied globally, which mean your whole application
* code including the third-party dependencies that also may use Carbon will
* adopt the new behavior.
*/
public static function enableFloatSetters(bool $floatSettersEnabled = true): void
{
self::$floatSettersEnabled = $floatSettersEnabled;
}

///////////////////////////////////////////////////////////////////
//////////////////////////// CONSTRUCTORS /////////////////////////
///////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -1225,51 +1244,63 @@ public function set($name, $value = null)
foreach ($properties as $key => $value) {
switch (Carbon::singularUnit(rtrim($key, 'z'))) {
case 'year':
$this->assertSafeForInteger($key, $value);
$this->checkIntegerValue($key, $value);
$this->y = $value;
$this->handleDecimalPart('year', $value, $this->y);

break;

case 'month':
$this->assertSafeForInteger($key, $value);
$this->checkIntegerValue($key, $value);
$this->m = $value;
$this->handleDecimalPart('month', $value, $this->m);

break;

case 'week':
$this->assertSafeForInteger($key, $value);
$this->d = $value * (int) static::getDaysPerWeek();
$this->checkIntegerValue($key, $value);
$days = $value * (int) static::getDaysPerWeek();
$this->assertSafeForInteger('days total (including weeks)', $days);
$this->d = $days;
$this->handleDecimalPart('day', $days, $this->d);

break;

case 'day':
$this->assertSafeForInteger($key, $value);
$this->checkIntegerValue($key, $value);
$this->d = $value;
$this->handleDecimalPart('day', $value, $this->d);

break;

case 'daysexcludeweek':
case 'dayzexcludeweek':
$this->assertSafeForInteger($key, $value);
$this->d = $this->weeks * (int) static::getDaysPerWeek() + $value;
$this->checkIntegerValue($key, $value);
$days = $this->weeks * (int) static::getDaysPerWeek() + $value;
$this->assertSafeForInteger('days total (including weeks)', $days);
$this->d = $days;
$this->handleDecimalPart('day', $days, $this->d);

break;

case 'hour':
$this->assertSafeForInteger($key, $value);
$this->checkIntegerValue($key, $value);
$this->h = $value;
$this->handleDecimalPart('hour', $value, $this->h);

break;

case 'minute':
$this->assertSafeForInteger($key, $value);
$this->checkIntegerValue($key, $value);
$this->i = $value;
$this->handleDecimalPart('minute', $value, $this->i);

break;

case 'second':
$this->assertSafeForInteger($key, $value);
$this->checkIntegerValue($key, $value);
$this->s = $value;
$this->handleDecimalPart('second', $value, $this->s);

break;

Expand Down Expand Up @@ -2930,6 +2961,31 @@ private function needsDeclension(string $mode, int $index, int $parts): bool
}
}

private function checkIntegerValue(string $name, $value)
{
if (\is_int($value)) {
return;
}

$this->assertSafeForInteger($name, $value);

if (\is_float($value) && (((float) (int) $value) === $value)) {
return;
}

if (!self::$floatSettersEnabled) {
$type = \gettype($value);
@trigger_error(
"Since 2.70.0, it's deprecated to pass $type value for $name.\n".
"It's truncated when stored as an integer interval unit.\n".
"From 3.0.0, decimal part will no longer be truncated and will be cascaded to smaller units.\n".
"- To maintain the current behavior, use explicit cast: $name((int) \$value)\n".
"- To adopt the new behavior globally, call CarbonInterval::enableFloatSetters()\n",
\E_USER_DEPRECATED
);
}
}

/**
* Throw an exception if precision loss when storing the given value as an integer would be >= 1.0.
*/
Expand All @@ -2939,4 +2995,43 @@ private function assertSafeForInteger(string $name, $value)
throw new OutOfRangeException($name, -0x7fffffffffffffff, 0x7fffffffffffffff, $value);
}
}

private function handleDecimalPart(string $unit, $value, $integerValue)
{
if (self::$floatSettersEnabled) {
$floatValue = (float) $value;
$base = (float) $integerValue;

if ($floatValue === $base) {
return;
}

$units = [
'y' => 'year',
'm' => 'month',
'd' => 'day',
'h' => 'hour',
'i' => 'minute',
's' => 'second',
];
$upper = true;

foreach ($units as $property => $name) {
if ($name === $unit) {
$upper = false;

continue;
}

if (!$upper && $this->$property !== 0) {
throw new RuntimeException(
"You cannot set $unit to a float value as $name would be overridden, ".
'set it first to 0 explicitly if you really want to erase its value'
);
}
}

$this->add($unit, $floatValue - $base);
}
}
}
6 changes: 5 additions & 1 deletion tests/CarbonInterval/ConstructTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ class ConstructTest extends AbstractTestCase
{
public function testInheritedConstruct()
{
CarbonInterval::createFromDateString('1 hour');
/** @phpstan-var CarbonInterval $ci */
$ci = CarbonInterval::createFromDateString('1 hour');
$this->assertSame('PT1H', $ci->spec());
$ci = new CarbonInterval('PT0S');
$this->assertSame('PT0S', $ci->spec());
$ci = new CarbonInterval('P1Y2M3D');
Expand All @@ -34,6 +36,8 @@ public function testInheritedConstruct()
$this->assertSame('PT0S', $ci->spec());
$ci = CarbonInterval::create('P1Y2M3D');
$this->assertSame('P1Y2M3D', $ci->spec());
$ci = CarbonInterval::create('P1Y2M3.0D');
$this->assertSame('P1Y2M3D', $ci->spec());
$ci = CarbonInterval::create('PT9.5H+85M');
$this->assertSame('PT9H115M', $ci->spec());
$ci = CarbonInterval::create('PT9H+85M');
Expand Down
68 changes: 68 additions & 0 deletions tests/CarbonInterval/FloatSettersEnabledTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

/**
* This file is part of the Carbon package.
*
* (c) Brian Nesbitt <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Tests\CarbonInterval;

use Carbon\CarbonInterval;
use Carbon\CarbonPeriod;
use RuntimeException;
use Tests\AbstractTestCase;

class FloatSettersEnabledTest extends AbstractTestCase
{
protected function setUp(): void
{
parent::setUp();
CarbonInterval::enableFloatSetters();
}

protected function tearDown(): void
{
CarbonInterval::enableFloatSetters(false);
parent::tearDown();
}

public function testInheritedConstruct()
{
$ci = new CarbonInterval('PT0S');
$ci->hours(0.5);
$this->assertSame('PT30M', $ci->spec());

$ci = new CarbonInterval('P1D');
$ci->hours(0.5);
$this->assertSame('P1DT30M', $ci->spec());

$ci = new CarbonInterval('PT4H');
$ci->hours(0.5);
$this->assertSame('PT30M', $ci->spec());

$period = CarbonPeriod::since('2018-04-21 00:00:00')->hours(0.5)->until('2018-04-21 02:00:00');
$this->assertSame('2018-04-21 00:30:00', $period->toArray()[1]->format('Y-m-d H:i:s'));

CarbonInterval::enableFloatSetters(false);
$ci = new CarbonInterval('PT4H');
$ci->hours(0.5);
$this->assertSame('PT0S', $ci->spec());
}

public function testOverridePrevention()
{
$this->expectExceptionObject(new RuntimeException(
'You cannot set hour to a float value as minute would be overridden, '.
'set it first to 0 explicitly if you really want to erase its value'
));

$ci = new CarbonInterval('PT10M');
$ci->hours(0.5);
}
}

0 comments on commit d3298b3

Please sign in to comment.