Skip to content

Commit

Permalink
Allow float in interval units
Browse files Browse the repository at this point in the history
  • Loading branch information
kylekatarnls committed Aug 26, 2023
1 parent 61efaa8 commit 8dc31f6
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 40 deletions.
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ parameters:
- '*/tests/CarbonPeriod/Fixtures/filters.php'
- '*/tests/Fixtures/dynamicInterval.php'
- '*/tests/PHPStan/*.php'
- '*/tests/PHPUnit/AssertObjectHasPropertyPolyfillTrait.php'
122 changes: 110 additions & 12 deletions src/Carbon/CarbonInterval.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Carbon\Exceptions\BadFluentSetterException;
use Carbon\Exceptions\InvalidCastException;
use Carbon\Exceptions\InvalidIntervalException;
use Carbon\Exceptions\OutOfRangeException;
use Carbon\Exceptions\ParseErrorException;
use Carbon\Exceptions\UnitNotConfiguredException;
use Carbon\Exceptions\UnknownGetterException;
Expand All @@ -32,6 +33,7 @@
use DateTimeInterface;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use ReflectionException;
use ReturnTypeWillChange;
use Throwable;
Expand Down Expand Up @@ -355,13 +357,13 @@ public static function setCascadeFactors(array $cascadeFactors)
* Create a new CarbonInterval instance.
*
* @param Closure|DateInterval|string|int|null $years
* @param int|null $months
* @param int|null $weeks
* @param int|null $days
* @param int|null $hours
* @param int|null $minutes
* @param int|null $seconds
* @param int|null $microseconds
* @param int|float|null $months
* @param int|float|null $weeks
* @param int|float|null $days
* @param int|float|null $hours
* @param int|float|null $minutes
* @param int|float|null $seconds
* @param int|float|null $microseconds
*
* @throws Exception when the interval_spec (passed as $years) cannot be parsed as an interval.
*/
Expand All @@ -381,8 +383,9 @@ public function __construct($years = 1, $months = null, $weeks = null, $days = n
}

$spec = $years;
$isStringSpec = (\is_string($spec) && !preg_match('/^[\d.]/', $spec));

if (!\is_string($spec) || (float) $years || preg_match('/^[\d.]/', $years)) {
if (!$isStringSpec || (float) $years) {
$spec = static::PERIOD_PREFIX;

$spec .= $years > 0 ? $years.static::PERIOD_YEARS : '';
Expand All @@ -407,7 +410,74 @@ public function __construct($years = 1, $months = null, $weeks = null, $days = n
}
}

parent::__construct($spec);
try {
parent::__construct($spec);
} catch (Throwable $exception) {
try {
parent::__construct('PT0S');

if ($isStringSpec) {
if (!preg_match('/^P
(?:(?<year>[+-]?\d*(?:\.\d+)?)Y)?
(?:(?<month>[+-]?\d*(?:\.\d+)?)M)?
(?:(?<week>[+-]?\d*(?:\.\d+)?)W)?
(?:(?<day>[+-]?\d*(?:\.\d+)?)D)?
(?:T
(?:(?<hour>[+-]?\d*(?:\.\d+)?)H)?
(?:(?<minute>[+-]?\d*(?:\.\d+)?)M)?
(?:(?<second>[+-]?\d*(?:\.\d+)?)S)?
)?
$/x', $spec, $match)) {
throw new InvalidArgumentException("Invalid duration: $spec");
}

$years = (float) ($match['year'] ?? 0);
$this->assertSafeForInteger('year', $years);
$months = (float) ($match['month'] ?? 0);
$this->assertSafeForInteger('month', $months);
$weeks = (float) ($match['week'] ?? 0);
$this->assertSafeForInteger('week', $weeks);
$days = (float) ($match['day'] ?? 0);
$this->assertSafeForInteger('day', $days);
$hours = (float) ($match['hour'] ?? 0);
$this->assertSafeForInteger('hour', $hours);
$minutes = (float) ($match['minute'] ?? 0);
$this->assertSafeForInteger('minute', $minutes);
$seconds = (float) ($match['second'] ?? 0);
$this->assertSafeForInteger('second', $seconds);
}

$totalDays = (($weeks * static::getDaysPerWeek()) + $days);
$this->assertSafeForInteger('days total (including weeks)', $totalDays);

$this->y = (int) $years;
$this->m = (int) $months;
$this->d = (int) $totalDays;
$this->h = (int) $hours;
$this->i = (int) $minutes;
$this->s = (int) $seconds;

if (
((float) $this->y) !== $years ||
((float) $this->m) !== $months ||
((float) $this->d) !== $totalDays ||
((float) $this->h) !== $hours ||
((float) $this->i) !== $minutes ||
((float) $this->s) !== $seconds
) {
$this->add(static::fromString(
($years - $this->y).' years '.
($months - $this->m).' months '.
($totalDays - $this->d).' days '.
($hours - $this->h).' hours '.
($minutes - $this->i).' minutes '.
($seconds - $this->s).' seconds '
));
}
} catch (Throwable $secondException) {
throw $secondException instanceof OutOfRangeException ? $secondException : $exception;
}
}

if ($microseconds !== null) {
$this->f = $microseconds / Carbon::MICROSECONDS_PER_SECOND;
Expand Down Expand Up @@ -953,11 +1023,18 @@ public function cast(string $className)
* set the $days field.
*
* @param DateInterval $interval
* @param bool $skipCopy set to true to return the passed object
* (without copying it) if it's already of the
* current class
*
* @return static
*/
public static function instance(DateInterval $interval, array $skip = [])
public static function instance(DateInterval $interval, array $skip = [], bool $skipCopy = false)
{
if ($skipCopy && $interval instanceof static) {
return $interval;
}

return self::castIntervalToClass($interval, static::class, $skip);
}

Expand All @@ -969,17 +1046,20 @@ public static function instance(DateInterval $interval, array $skip = [])
*
* @param mixed|int|DateInterval|string|Closure|null $interval interval or number of the given $unit
* @param string|null $unit if specified, $interval must be an integer
* @param bool $skipCopy set to true to return the passed object
* (without copying it) if it's already of the
* current class
*
* @return static|null
*/
public static function make($interval, $unit = null)
public static function make($interval, $unit = null, bool $skipCopy = false)
{
if ($unit) {
$interval = "$interval ".Carbon::pluralUnit($unit);
}

if ($interval instanceof DateInterval) {
return static::instance($interval);
return static::instance($interval, [], $skipCopy);
}

if ($interval instanceof Closure) {
Expand Down Expand Up @@ -1145,42 +1225,50 @@ public function set($name, $value = null)
foreach ($properties as $key => $value) {
switch (Carbon::singularUnit(rtrim($key, 'z'))) {
case 'year':
$this->assertSafeForInteger($key, $value);
$this->y = $value;

break;

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

break;

case 'week':
$this->assertSafeForInteger($key, $value);
$this->d = $value * (int) static::getDaysPerWeek();

break;

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

break;

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

break;

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

break;

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

break;

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

break;
Expand Down Expand Up @@ -2841,4 +2929,14 @@ private function needsDeclension(string $mode, int $index, int $parts): bool
return true;
}
}

/**
* Throw an exception if precision loss when storing the given value as an integer would be >= 1.0.
*/
private function assertSafeForInteger(string $name, $value)
{
if ($value && !\is_int($value) && ($value >= 0x7fffffffffffffff || $value <= -0x7fffffffffffffff)) {
throw new OutOfRangeException($name, -0x7fffffffffffffff, 0x7fffffffffffffff, $value);
}
}
}
2 changes: 1 addition & 1 deletion src/Carbon/Traits/IntervalRounding.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected function roundWith($precision, $function)
$unit = 'second';

if ($precision instanceof DateInterval) {
$precision = (string) CarbonInterval::instance($precision);
$precision = (string) CarbonInterval::instance($precision, [], true);
}

if (\is_string($precision) && preg_match('/^\s*(?<precision>\d+)?\s*(?<unit>\w+)(?<other>\W.*)?$/', $precision, $match)) {
Expand Down
6 changes: 3 additions & 3 deletions src/Carbon/Traits/Units.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public function rawAdd(DateInterval $interval)
public function add($unit, $value = 1, $overflow = null)
{
if (\is_string($unit) && \func_num_args() === 1) {
$unit = CarbonInterval::make($unit);
$unit = CarbonInterval::make($unit, [], true);
}

if ($unit instanceof CarbonConverterInterface) {
Expand Down Expand Up @@ -368,7 +368,7 @@ public function rawSub(DateInterval $interval)
public function sub($unit, $value = 1, $overflow = null)
{
if (\is_string($unit) && \func_num_args() === 1) {
$unit = CarbonInterval::make($unit);
$unit = CarbonInterval::make($unit, [], true);
}

if ($unit instanceof CarbonConverterInterface) {
Expand Down Expand Up @@ -404,7 +404,7 @@ public function sub($unit, $value = 1, $overflow = null)
public function subtract($unit, $value = 1, $overflow = null)
{
if (\is_string($unit) && \func_num_args() === 1) {
$unit = CarbonInterval::make($unit);
$unit = CarbonInterval::make($unit, [], true);
}

return $this->sub($unit, $value, $overflow);
Expand Down
3 changes: 3 additions & 0 deletions tests/AbstractTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
use DateTime;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;
use Tests\PHPUnit\AssertObjectHasPropertyTrait;
use Throwable;

/**
* @SuppressWarnings(PHPMD.NumberOfChildren)
*/
abstract class AbstractTestCase extends TestCase
{
use AssertObjectHasPropertyTrait;

/**
* @var \Carbon\Carbon
*/
Expand Down
24 changes: 12 additions & 12 deletions tests/Carbon/ObjectsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,40 +28,40 @@ public function testToObject()

$this->assertInstanceOf(stdClass::class, $dtToObject);

$this->assertObjectHasAttribute('year', $dtToObject);
$this->assertObjectHasProperty('year', $dtToObject);
$this->assertSame($dt->year, $dtToObject->year);

$this->assertObjectHasAttribute('month', $dtToObject);
$this->assertObjectHasProperty('month', $dtToObject);
$this->assertSame($dt->month, $dtToObject->month);

$this->assertObjectHasAttribute('day', $dtToObject);
$this->assertObjectHasProperty('day', $dtToObject);
$this->assertSame($dt->day, $dtToObject->day);

$this->assertObjectHasAttribute('dayOfWeek', $dtToObject);
$this->assertObjectHasProperty('dayOfWeek', $dtToObject);
$this->assertSame($dt->dayOfWeek, $dtToObject->dayOfWeek);

$this->assertObjectHasAttribute('dayOfYear', $dtToObject);
$this->assertObjectHasProperty('dayOfYear', $dtToObject);
$this->assertSame($dt->dayOfYear, $dtToObject->dayOfYear);

$this->assertObjectHasAttribute('hour', $dtToObject);
$this->assertObjectHasProperty('hour', $dtToObject);
$this->assertSame($dt->hour, $dtToObject->hour);

$this->assertObjectHasAttribute('minute', $dtToObject);
$this->assertObjectHasProperty('minute', $dtToObject);
$this->assertSame($dt->minute, $dtToObject->minute);

$this->assertObjectHasAttribute('second', $dtToObject);
$this->assertObjectHasProperty('second', $dtToObject);
$this->assertSame($dt->second, $dtToObject->second);

$this->assertObjectHasAttribute('micro', $dtToObject);
$this->assertObjectHasProperty('micro', $dtToObject);
$this->assertSame($dt->micro, $dtToObject->micro);

$this->assertObjectHasAttribute('timestamp', $dtToObject);
$this->assertObjectHasProperty('timestamp', $dtToObject);
$this->assertSame($dt->timestamp, $dtToObject->timestamp);

$this->assertObjectHasAttribute('timezone', $dtToObject);
$this->assertObjectHasProperty('timezone', $dtToObject);
$this->assertEquals($dt->timezone, $dtToObject->timezone);

$this->assertObjectHasAttribute('formatted', $dtToObject);
$this->assertObjectHasProperty('formatted', $dtToObject);
$this->assertSame($dt->format(Carbon::DEFAULT_TO_STRING_FORMAT), $dtToObject->formatted);
}

Expand Down
24 changes: 12 additions & 12 deletions tests/CarbonImmutable/ObjectsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,40 +28,40 @@ public function testToObject()

$this->assertInstanceOf(stdClass::class, $dtToObject);

$this->assertObjectHasAttribute('year', $dtToObject);
$this->assertObjectHasProperty('year', $dtToObject);
$this->assertSame($dt->year, $dtToObject->year);

$this->assertObjectHasAttribute('month', $dtToObject);
$this->assertObjectHasProperty('month', $dtToObject);
$this->assertSame($dt->month, $dtToObject->month);

$this->assertObjectHasAttribute('day', $dtToObject);
$this->assertObjectHasProperty('day', $dtToObject);
$this->assertSame($dt->day, $dtToObject->day);

$this->assertObjectHasAttribute('dayOfWeek', $dtToObject);
$this->assertObjectHasProperty('dayOfWeek', $dtToObject);
$this->assertSame($dt->dayOfWeek, $dtToObject->dayOfWeek);

$this->assertObjectHasAttribute('dayOfYear', $dtToObject);
$this->assertObjectHasProperty('dayOfYear', $dtToObject);
$this->assertSame($dt->dayOfYear, $dtToObject->dayOfYear);

$this->assertObjectHasAttribute('hour', $dtToObject);
$this->assertObjectHasProperty('hour', $dtToObject);
$this->assertSame($dt->hour, $dtToObject->hour);

$this->assertObjectHasAttribute('minute', $dtToObject);
$this->assertObjectHasProperty('minute', $dtToObject);
$this->assertSame($dt->minute, $dtToObject->minute);

$this->assertObjectHasAttribute('second', $dtToObject);
$this->assertObjectHasProperty('second', $dtToObject);
$this->assertSame($dt->second, $dtToObject->second);

$this->assertObjectHasAttribute('micro', $dtToObject);
$this->assertObjectHasProperty('micro', $dtToObject);
$this->assertSame($dt->micro, $dtToObject->micro);

$this->assertObjectHasAttribute('timestamp', $dtToObject);
$this->assertObjectHasProperty('timestamp', $dtToObject);
$this->assertSame($dt->timestamp, $dtToObject->timestamp);

$this->assertObjectHasAttribute('timezone', $dtToObject);
$this->assertObjectHasProperty('timezone', $dtToObject);
$this->assertEquals($dt->timezone, $dtToObject->timezone);

$this->assertObjectHasAttribute('formatted', $dtToObject);
$this->assertObjectHasProperty('formatted', $dtToObject);
$this->assertSame($dt->format(Carbon::DEFAULT_TO_STRING_FORMAT), $dtToObject->formatted);
}

Expand Down
Loading

0 comments on commit 8dc31f6

Please sign in to comment.