feast(services): add duration solver

This commit is contained in:
Ondrej Vlach 2024-08-07 12:53:46 +02:00
parent 332d4b2dce
commit ef937530af
No known key found for this signature in database
GPG Key ID: 7F141CDACEDEE2DE
3 changed files with 148 additions and 0 deletions

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Services\WorkingDays;
use App\Services\Utils\DurationConvertor;
/**
* Solves the duration of a given period based on working days and return new end date.
*/
class DurationSolver
{
protected const int MAX_ITERATIONS = 100;
public function __construct(
protected readonly WorkingDayDeterminerInterface $workingDayDeterminer,
protected readonly DurationConvertor $durationConvertor,
) {
}
/**
* Calculate the duration of a given period based on working days.
* @param \DateTimeImmutable $startDate
* @param \DateTimeImmutable $endDate
* @return \DateTimeImmutable
* @throws \Exception
*/
public function calculateDuration(
\DateTimeImmutable $startDate,
\DateTimeImmutable $endDate,
): \DateTimeImmutable {
$loopId = 0;
$requiredWorkingDaysCount = (int) ($startDate->setTime(0, 0)->diff($endDate->setTime(0, 0))->format('%a')) + 1;
do {
$workingDaysCount = (int) ($startDate->setTime(0, 0)->diff($endDate->setTime(0, 0))->format('%a')) + 1;
$workingDaysCountInInterval = $this->workingDayDeterminer->getWorkingDaysCount($startDate, $workingDaysCount);
if ($workingDaysCountInInterval >= $requiredWorkingDaysCount) {
// there is no weekends or holidays in the given interval
break;
} else {
$endDate = $endDate->add(new \DateInterval(sprintf('P%dD', $requiredWorkingDaysCount - $workingDaysCountInInterval)));
if ($loopId > static::MAX_ITERATIONS) {
throw new \InvalidArgumentException('No working days found in the given interval');
}
}
$loopId++;
} while ($workingDaysCount != $workingDaysCountInInterval);
return $endDate;
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Services\WorkingDays;
use App\Services\Utils\DurationConvertor;
class DurationSolverFactory
{
public function __construct(
private readonly WorkingDayDeterminerFactory $workingDayDeterminerFactory,
private readonly DurationConvertor $durationConvertor,
) {
}
/**
* Create DurationSolver instance for a specific country
* @param string $countryCode
* @return DurationSolver
*/
public function createDurationSolverForCountry(string $countryCode): DurationSolver
{
return new DurationSolver(
$this->workingDayDeterminerFactory->createForCountry($countryCode),
$this->durationConvertor,
);
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\WorkingDays;
use App\Services\Utils\DurationConvertor;
use App\Services\WorkingDays\DurationSolver;
use App\Services\WorkingDays\WorkingDayDeterminerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class DurationSolverTest extends TestCase
{
/**
* @return array<array<int|array<int>>>
*/
public static function solverDurationProvider(): array
{
// 4 days is required state
return [
[3, [4]], /* without holidays */
[4, [3, 4]], /* 1 day */
[6, [2, 3, 4]], /* 1 weekend, monday holiday */
[5, [2, 4]], /* 1 weekend */
];
}
/**
* @param int $expectedResult
* @param array<int> $solverResults
* @return void
* @throws \PHPUnit\Framework\MockObject\Exception
*/
#[DataProvider('solverDurationProvider')]
public function testSolverWillSolveDuration(int $expectedResult, array $solverResults): void
{
$workingDayDeterminer = $this->createMock(WorkingDayDeterminerInterface::class);
$workingDayDeterminer->method('getWorkingDaysCount')->willReturnCallback(function () use ($solverResults) {
static $calledCount = 0;
return $solverResults[$calledCount++] ?? throw new \Exception("Not enough solver results for try $calledCount" . print_r($solverResults, true));
});
$durationSolver = new DurationSolver(
$workingDayDeterminer,
new DurationConvertor()
);
$this->assertEquals(
(new \DateTimeImmutable('2022-01-01 23:59:59'))->add(new \DateInterval(sprintf('P%dD', $expectedResult))),
$durationSolver->calculateDuration(new \DateTimeImmutable('2022-01-01 00:00:00'), new \DateTimeImmutable('2022-01-04 23:59:59'))
);
}
public function testDurationSolverWillThrowExceptionWhenCantSolveDuration(): void
{
$workingDayDeterminer = $this->createMock(WorkingDayDeterminerInterface::class);
$workingDayDeterminer->method('getWorkingDaysCount')->willReturn(0);
$durationSolver = new DurationSolver(
$workingDayDeterminer,
new DurationConvertor()
);
$this->expectException(\InvalidArgumentException::class);
$durationSolver->calculateDuration(new \DateTimeImmutable('2022-01-01 00:00:00'), new \DateTimeImmutable('2022-01-04 23:59:59'));
}
}