Upstream briannesbitt/Carbon state of the touched files before 56c9340e26

This commit is contained in:
upstream committed 2026-09-04 04:29:29 +02:00
commit 06596ca5df
2 files changed
+434

No files matched your search

+226
View File
@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
/**
* This file is part of the Carbon package.
*
* (c) Brian Nesbitt <brian@nesbot.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carbon\Traits;
use Carbon\CarbonInterface;
use Carbon\Exceptions\UnknownUnitException;
use Carbon\WeekDay;
use DateInterval;
/**
* Trait Rounding.
*
* Round, ceil, floor units.
*
* Depends on the following methods:
*
* @method static copy()
* @method static startOfWeek(int $weekStartsAt = null)
*/
trait Rounding
{
use IntervalRounding;
/**
* Round the current instance at the given unit with given precision if specified and the given function.
*/
public function roundUnit(
string $unit,
DateInterval|string|float|int $precision = 1,
callable|string $function = 'round',
): static {
$metaUnits = [
// @call roundUnit
'millennium' => [static::YEARS_PER_MILLENNIUM, 'year'],
// @call roundUnit
'century' => [static::YEARS_PER_CENTURY, 'year'],
// @call roundUnit
'decade' => [static::YEARS_PER_DECADE, 'year'],
// @call roundUnit
'quarter' => [static::MONTHS_PER_QUARTER, 'month'],
// @call roundUnit
'millisecond' => [1000, 'microsecond'],
];
$normalizedUnit = static::singularUnit($unit);
$ranges = array_merge(static::getRangesByUnit($this->daysInMonth), [
// @call roundUnit
'microsecond' => [0, 999999],
]);
$factor = 1;
if ($normalizedUnit === 'week') {
$normalizedUnit = 'day';
$precision *= static::DAYS_PER_WEEK;
}
if (isset($metaUnits[$normalizedUnit])) {
[$factor, $normalizedUnit] = $metaUnits[$normalizedUnit];
}
$precision *= $factor;
if (!isset($ranges[$normalizedUnit])) {
throw new UnknownUnitException($unit);
}
$found = false;
$fraction = 0;
$arguments = null;
$initialValue = null;
$factor = $this->year < 0 ? -1 : 1;
$changes = [];
$minimumInc = null;
foreach ($ranges as $unit => [$minimum, $maximum]) {
if ($normalizedUnit === $unit) {
$arguments = [$this->$unit, $minimum];
$initialValue = $this->$unit;
$fraction = $precision - floor($precision);
$found = true;
continue;
}
if ($found) {
$delta = $maximum + 1 - $minimum;
$factor /= $delta;
$fraction *= $delta;
$inc = ($this->$unit - $minimum) * $factor;
if ($inc !== 0.0) {
$minimumInc = $minimumInc ?? ($arguments[0] / pow(2, 52));
// If value is still the same when adding a non-zero increment/decrement,
// it means precision got lost in the addition
if (abs($inc) < $minimumInc) {
$inc = $minimumInc * ($inc < 0 ? -1 : 1);
}
// If greater than $precision, assume precision loss caused an overflow
if ($function !== 'floor' || abs($arguments[0] + $inc - $initialValue) >= $precision) {
$arguments[0] += $inc;
}
}
$changes[$unit] = round(
$minimum + ($fraction ? $fraction * $function(($this->$unit - $minimum) / $fraction) : 0),
);
// Cannot use modulo as it lose double precision
while ($changes[$unit] >= $delta) {
$changes[$unit] -= $delta;
}
$fraction -= floor($fraction);
}
}
[$value, $minimum] = $arguments;
$normalizedValue = floor($function(($value - $minimum) / $precision) * $precision + $minimum);
/** @var CarbonInterface $result */
$result = $this;
foreach ($changes as $unit => $value) {
$result = $result->$unit($value);
}
return $result->$normalizedUnit($normalizedValue);
}
/**
* Truncate the current instance at the given unit with given precision if specified.
*/
public function floorUnit(string $unit, DateInterval|string|float|int $precision = 1): static
{
return $this->roundUnit($unit, $precision, 'floor');
}
/**
* Ceil the current instance at the given unit with given precision if specified.
*/
public function ceilUnit(string $unit, DateInterval|string|float|int $precision = 1): static
{
return $this->roundUnit($unit, $precision, 'ceil');
}
/**
* Round the current instance second with given precision if specified.
*/
public function round(DateInterval|string|float|int $precision = 1, callable|string $function = 'round'): static
{
return $this->roundWith($precision, $function);
}
/**
* Round the current instance second with given precision if specified.
*/
public function floor(DateInterval|string|float|int $precision = 1): static
{
return $this->round($precision, 'floor');
}
/**
* Ceil the current instance second with given precision if specified.
*/
public function ceil(DateInterval|string|float|int $precision = 1): static
{
return $this->round($precision, 'ceil');
}
/**
* Round the current instance week.
*
* @param WeekDay|int|null $weekStartsAt optional start allow you to specify the day of week to use to start the week
*/
public function roundWeek(WeekDay|int|null $weekStartsAt = null): static
{
return $this->closest(
$this->avoidMutation()->floorWeek($weekStartsAt),
$this->avoidMutation()->ceilWeek($weekStartsAt),
);
}
/**
* Truncate the current instance week.
*
* @param WeekDay|int|null $weekStartsAt optional start allow you to specify the day of week to use to start the week
*/
public function floorWeek(WeekDay|int|null $weekStartsAt = null): static
{
return $this->startOfWeek($weekStartsAt);
}
/**
* Ceil the current instance week.
*
* @param WeekDay|int|null $weekStartsAt optional start allow you to specify the day of week to use to start the week
*/
public function ceilWeek(WeekDay|int|null $weekStartsAt = null): static
{
if ($this->isMutable()) {
$startOfWeek = $this->avoidMutation()->startOfWeek($weekStartsAt);
return $startOfWeek != $this ?
$this->startOfWeek($weekStartsAt)->addWeek() :
$this;
}
$startOfWeek = $this->startOfWeek($weekStartsAt);
return $startOfWeek != $this ?
$startOfWeek->addWeek() :
$this->avoidMutation();
}
}
+208
View File
@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
/**
* This file is part of the Carbon package.
*
* (c) Brian Nesbitt <brian@nesbot.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Carbon;
use Carbon\Carbon;
use Carbon\CarbonInterval;
use DateInterval;
use InvalidArgumentException;
use Tests\AbstractTestCase;
class RoundTest extends AbstractTestCase
{
public function testRoundWithDefaultUnit()
{
$dt = Carbon::create(2315, 7, 18, 22, 42, 17.643971);
$copy = $dt->copy();
$ref = $copy->round();
$this->assertSame($ref, $copy);
$this->assertCarbon($ref, 2315, 7, 18, 22, 42, 18, 0);
$this->assertCarbon($dt->copy()->round(5), 2315, 7, 18, 22, 42, 20, 0);
$this->assertCarbon($dt->copy()->floor()->round(5), 2315, 7, 18, 22, 42, 15, 0);
$this->assertCarbon($dt->copy()->round(3), 2315, 7, 18, 22, 42, 18, 0);
$this->assertCarbon($dt->copy()->round(4), 2315, 7, 18, 22, 42, 16, 0);
$this->assertCarbon($dt->copy()->round(10), 2315, 7, 18, 22, 42, 20, 0);
$this->assertCarbon($dt->copy()->round(0.5), 2315, 7, 18, 22, 42, 17, 500000);
$this->assertCarbon($dt->copy()->round(0.25), 2315, 7, 18, 22, 42, 17, 750000);
$this->assertCarbon($dt->copy()->round(3.8), 2315, 7, 18, 22, 42, 19, 800000);
$this->assertCarbon($dt->copy()->floor(5), 2315, 7, 18, 22, 42, 15, 0);
$this->assertCarbon($dt->copy()->floor()->floor(5), 2315, 7, 18, 22, 42, 15, 0);
$this->assertCarbon($dt->copy()->floor(3), 2315, 7, 18, 22, 42, 15, 0);
$this->assertCarbon($dt->copy()->floor(4), 2315, 7, 18, 22, 42, 16, 0);
$this->assertCarbon($dt->copy()->floor(10), 2315, 7, 18, 22, 42, 10, 0);
$this->assertCarbon($dt->copy()->floor(0.5), 2315, 7, 18, 22, 42, 17, 500000);
$this->assertCarbon($dt->copy()->floor(0.25), 2315, 7, 18, 22, 42, 17, 500000);
$this->assertCarbon($dt->copy()->floor(3.8), 2315, 7, 18, 22, 42, 15, 0);
$this->assertCarbon($dt->copy()->ceil(5), 2315, 7, 18, 22, 42, 20, 0);
$this->assertCarbon($dt->copy()->floor()->ceil(5), 2315, 7, 18, 22, 42, 20, 0);
$this->assertCarbon($dt->copy()->ceil(3), 2315, 7, 18, 22, 42, 18, 0);
$this->assertCarbon($dt->copy()->ceil(4), 2315, 7, 18, 22, 42, 20, 0);
$this->assertCarbon($dt->copy()->ceil(10), 2315, 7, 18, 22, 42, 20, 0);
$this->assertCarbon($dt->copy()->ceil(0.5), 2315, 7, 18, 22, 42, 18, 0);
$this->assertCarbon($dt->copy()->ceil(0.25), 2315, 7, 18, 22, 42, 17, 750000);
$this->assertCarbon($dt->copy()->ceil(3.8), 2315, 7, 18, 22, 42, 19, 800000);
}
public function testRoundWithStrings()
{
$dt = Carbon::create(2315, 7, 18, 22, 42, 17.643971);
$this->assertCarbon($dt->copy()->round('minute'), 2315, 7, 18, 22, 42, 0, 0);
$this->assertCarbon($dt->copy()->floor('5 minutes'), 2315, 7, 18, 22, 40, 0, 0);
$this->assertCarbon($dt->copy()->ceil('5 minutes'), 2315, 7, 18, 22, 45, 0, 0);
}
public function testRoundWithStringsException()
{
$this->expectExceptionObject(new InvalidArgumentException(
'Rounding is only possible with single unit intervals.',
));
Carbon::create(2315, 7, 18, 22, 42, 17.643971)->round('2 hours 5 minutes');
}
public function testRoundWithInterval()
{
$dt = Carbon::create(2315, 7, 18, 22, 42, 17.643971);
$this->assertCarbon($dt->copy()->round(CarbonInterval::minute()), 2315, 7, 18, 22, 42, 0, 0);
$this->assertCarbon($dt->copy()->floor(CarbonInterval::minutes(5)), 2315, 7, 18, 22, 40, 0, 0);
$this->assertCarbon($dt->copy()->ceil(new DateInterval('PT5M')), 2315, 7, 18, 22, 45, 0, 0);
}
public function testRoundWithIntervalException()
{
$this->expectExceptionObject(new InvalidArgumentException(
'Rounding is only possible with single unit intervals.',
));
Carbon::create(2315, 7, 18, 22, 42, 17.643971)->round(CarbonInterval::day()->minutes(5));
}
public function testRoundWithBaseUnit()
{
$dt = Carbon::create(2315, 7, 18, 22, 42, 17.643971);
$copy = $dt->copy();
$ref = $copy->roundSecond();
$this->assertSame($ref, $copy);
$this->assertCarbon($ref, 2315, 7, 18, 22, 42, 18, 0);
$this->assertCarbon($dt->copy()->roundDay(), 2315, 7, 19, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->roundDay(5), 2315, 7, 21, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->ceilDay(), 2315, 7, 19, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->floorDay(), 2315, 7, 18, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->roundYear(), 2316, 1, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->subMonths(2)->roundYear(), 2315, 1, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->roundYear(2), 2315, 1, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->floorYear(2), 2315, 1, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->ceilYear(2), 2317, 1, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->roundMonth(), 2315, 8, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->floorMonth(), 2315, 7, 1, 0, 0, 0, 0);
for ($i = 1; $i <= Carbon::MONTHS_PER_YEAR; $i++) {
$dt = Carbon::parse("2021-$i-01")->endOfMonth()->floorMonth();
$this->assertCarbon($dt, 2021, $i, 1, 0, 0, 0, 0);
}
}
public function testFloorYear()
{
$date = Carbon::create(2022)->endOfYear()->floorYear();
$this->assertCarbon($date, 2022, 1, 1, 0, 0, 0, 0);
$date = Carbon::create(2022)->endOfYear()->floorDay()->floorYear();
$this->assertCarbon($date, 2022, 1, 1, 0, 0, 0, 0);
$date = Carbon::create(2022)->endOfYear()->floorYear();
$this->assertCarbon($date, 2022, 1, 1, 0, 0, 0, 0);
$date = Carbon::create(2022)->addMonths(6)->floorYear();
$this->assertCarbon($date, 2022, 1, 1, 0, 0, 0, 0);
}
public function testCeilYear()
{
$date = Carbon::create(2022)->addMonths(6)->ceilYear();
$this->assertCarbon($date, 2023, 1, 1, 0, 0, 0, 0);
$date = Carbon::create(2022)->endOfYear()->ceilYear();
$this->assertCarbon($date, 2023, 1, 1, 0, 0, 0, 0);
$date = Carbon::create(2022)->ceilYear();
$this->assertCarbon($date, 2022, 1, 1, 0, 0, 0, 0);
$date = Carbon::create(2022)->addMicrosecond()->ceilYear();
$this->assertCarbon($date, 2023, 1, 1, 0, 0, 0, 0);
}
public function testRoundWithMetaUnit()
{
$dt = Carbon::create(2315, 7, 18, 22, 42, 17.643971);
$copy = $dt->copy();
$ref = $copy->roundSecond();
$this->assertSame($ref, $copy);
$this->assertCarbon($ref, 2315, 7, 18, 22, 42, 18, 0);
$this->assertCarbon($dt->copy()->roundMillisecond(), 2315, 7, 18, 22, 42, 17, 644000);
$this->assertCarbon($dt->copy()->roundMillennium(), 2001, 1, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->roundQuarter(), 2315, 7, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->roundQuarters(2), 2315, 7, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->subMonth()->floorQuarter(), 2315, 4, 1, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->subMonth()->floorQuarters(2), 2315, 1, 1, 0, 0, 0, 0);
}
public function testRoundWeek()
{
$dt = Carbon::create(2315, 7, 18, 22, 42, 17.643971);
$copy = $dt->copy();
$ref = $copy->roundSecond();
$this->assertSame($ref, $copy);
$this->assertCarbon($ref, 2315, 7, 18, 22, 42, 18, 0);
$this->assertCarbon($dt->copy()->floorWeek(), 2315, 7, 12, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->ceilWeek(), 2315, 7, 19, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->roundWeek(), 2315, 7, 19, 0, 0, 0, 0);
$dt = Carbon::create(2315, 7, 19, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->floorWeek(), 2315, 7, 19, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->ceilWeek(), 2315, 7, 19, 0, 0, 0, 0);
$this->assertCarbon($dt->copy()->roundWeek(), 2315, 7, 19, 0, 0, 0, 0);
}
public function testCeilMonth()
{
$this->assertCarbon(Carbon::parse('2021-01-29')->ceilMonth(), 2021, 2, 1, 0, 0, 0);
$this->assertCarbon(Carbon::parse('2021-01-31')->ceilMonth(), 2021, 2, 1, 0, 0, 0);
$this->assertCarbon(Carbon::parse('2021-12-17')->ceilMonth(), 2022, 1, 1, 0, 0, 0);
}
public function testFloorMonth()
{
$this->assertCarbon(Carbon::parse('2021-05-31')->floorMonth(3), 2021, 4, 1, 0, 0, 0);
}
public function testRoundInvalidArgument()
{
$this->expectExceptionObject(new InvalidArgumentException(
'Unknown unit \'foobar\'.',
));
Carbon::now()->roundUnit('foobar');
}
}