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 a8409e2
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 16 deletions.
130 changes: 118 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 Down Expand Up @@ -355,13 +356,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 +382,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 +409,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");

Check failure on line 430 in src/Carbon/CarbonInterval.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Instantiated class Carbon\InvalidArgumentException not found.

Check failure on line 430 in src/Carbon/CarbonInterval.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Throwing object of an unknown class Carbon\InvalidArgumentException.
}

$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 $_) {
throw $exception;
}
}

if ($microseconds !== null) {
$this->f = $microseconds / Carbon::MICROSECONDS_PER_SECOND;
Expand Down Expand Up @@ -953,11 +1022,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 +1045,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 +1224,50 @@ public function set($name, $value = null)
foreach ($properties as $key => $value) {
switch (Carbon::singularUnit(rtrim($key, 'z'))) {
case 'year':
$this->checkIntegerValue($name, $value);
$this->y = $value;

break;

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

break;

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

break;

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

break;

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

break;

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

break;

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

break;

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

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

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

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

/**
* 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 && ($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
33 changes: 33 additions & 0 deletions tests/CarbonInterval/ConstructTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
use Carbon\Carbon;
use Carbon\CarbonInterval;
use DateInterval;
use Exception;
use Tests\AbstractTestCase;

class ConstructTest extends AbstractTestCase
{
public function testInheritedConstruct()
{
CarbonInterval::createFromDateString('1 hour');
$ci = new CarbonInterval('PT0S');
$this->assertSame('PT0S', $ci->spec());
$ci = new CarbonInterval('P1Y2M3D');
Expand All @@ -31,6 +33,10 @@ public function testInheritedConstruct()
$this->assertSame('PT0S', $ci->spec());
$ci = CarbonInterval::create('P1Y2M3D');
$this->assertSame('P1Y2M3D', $ci->spec());
$ci = CarbonInterval::create('PT9.5H+85M');
$this->assertSame('PT9H115M', $ci->spec());
$ci = CarbonInterval::create('PT1999999999999.5H+85M');
$this->assertSame('PT1999999999999H115M', $ci->spec());
}

public function testConstructWithDateInterval()
Expand Down Expand Up @@ -315,6 +321,33 @@ public function testMake()
$this->assertSame(3000, CarbonInterval::make('3 millennia')->totalYears);
}

/**
* @dataProvider getBadFormats
*/
public function testBadFormats(string $format)
{
$this->expectExceptionObject(new Exception(
'Unknown or bad format (PT999999999999999999999999H)'
));

CarbonInterval::create('PT999999999999999999999999H');
}

public function getBadFormats()
{
$badFormats = [
'PT1999999999999.5.5H+85M',
'PT999999999999999999999999H',
];

return array_combine($badFormats, array_map(
static function (string $format) {
return [$format];
},
$badFormats,
));
}

public function testCallInvalidStaticMethod()
{
$this->expectExceptionObject(new BadMethodCallException(
Expand Down
5 changes: 5 additions & 0 deletions tests/CarbonInterval/FromStringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ public static function dataForValidStrings(): Generator

// case insensitive
yield ['1Y 3MO 1W 3D 12H 23M 42S', new CarbonInterval(1, 3, 1, 3, 12, 23, 42)];

// big numbers
yield ['1999999999999.5 hours', new CarbonInterval(0, 0, 0, 0, 1999999999999, 30, 0)];
yield [(0x7fffffffffffffff).' days', new CarbonInterval(0, 0, 0, 0x7fffffffffffffff, 0, 0, 0)];
yield ['1999999999999.5 hours -85 minutes', new CarbonInterval(0, 0, 0, 0, 1999999999999, 115, 0)];
}

/**
Expand Down

0 comments on commit a8409e2

Please sign in to comment.