mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2024-11-26 02:40:43 -06:00
Add the Calendar Calculator
It encapsulates some date operations like sum. The result will be the calculated date when calling the nextDateByInterval method, given the date, periodicity, and skipInterval parameters. For example, given a date of 2019-12-31, monthly periodicity, and skip interval 0, the results will be 2020-01-31. Also, if the skip interval is 1, the result is 2020-02-29. This is because the next date will add another month to the current range.
This commit is contained in:
parent
4e3c2ba72c
commit
dbb7ed3d5d
81
app/Support/Calendar/Calculator.php
Normal file
81
app/Support/Calendar/Calculator.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Support\Calendar;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Support\Calendar\Exceptions\IntervalException;
|
||||
|
||||
class Calculator
|
||||
{
|
||||
const DEFAULT_INTERVAL = 1;
|
||||
private static array $intervals = [];
|
||||
private static ?\SplObjectStorage $intervalMap = null;
|
||||
|
||||
private static function loadIntervalMap(): \SplObjectStorage
|
||||
{
|
||||
if (self::$intervalMap != null) {
|
||||
return self::$intervalMap;
|
||||
}
|
||||
self::$intervalMap = new \SplObjectStorage();
|
||||
foreach (Periodicity::cases() as $interval) {
|
||||
$periodicityClass = __NAMESPACE__ . "\\Periodicity\\{$interval->name}";
|
||||
self::$intervals[] = $interval->name;
|
||||
self::$intervalMap->attach($interval, new $periodicityClass());
|
||||
}
|
||||
return self::$intervalMap;
|
||||
}
|
||||
|
||||
private static function containsInterval(Periodicity $periodicity): bool
|
||||
{
|
||||
return self::loadIntervalMap()->contains($periodicity);
|
||||
}
|
||||
|
||||
public function isAvailablePeriodicity(Periodicity $periodicity): bool
|
||||
{
|
||||
return self::containsInterval($periodicity);
|
||||
}
|
||||
|
||||
private function skipInterval(int $skip): int
|
||||
{
|
||||
return self::DEFAULT_INTERVAL + $skip;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Carbon $epoch
|
||||
* @param Periodicity $periodicity
|
||||
* @param int $skipInterval
|
||||
* @return Carbon
|
||||
* @throws IntervalException
|
||||
*/
|
||||
public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
|
||||
{
|
||||
if (!self::isAvailablePeriodicity($periodicity)) {
|
||||
throw IntervalException::unavailable($periodicity, self::$intervals);
|
||||
}
|
||||
|
||||
/** @var Periodicity\Interval $periodicity */
|
||||
$periodicity = self::$intervalMap->offsetGet($periodicity);
|
||||
$interval = $this->skipInterval($skipInterval);
|
||||
return $periodicity->nextDate($epoch->clone(), $interval);
|
||||
}
|
||||
|
||||
}
|
28
app/Support/Calendar/Exceptions/IntervalException.php
Normal file
28
app/Support/Calendar/Exceptions/IntervalException.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace FireflyIII\Support\Calendar\Exceptions;
|
||||
|
||||
use FireflyIII\Support\Calendar\Periodicity;
|
||||
use JetBrains\PhpStorm\Pure;
|
||||
|
||||
final class IntervalException extends \Exception
|
||||
{
|
||||
protected $message = 'The periodicity %s is unknown. Choose one of available periodicity: %s';
|
||||
|
||||
public readonly Periodicity $periodicity;
|
||||
public readonly array $availableIntervals;
|
||||
|
||||
public static function unavailable(Periodicity $periodicity, array $instervals, int $code = 0, ?\Throwable $previous = null): IntervalException
|
||||
{
|
||||
$message = sprintf(
|
||||
'The periodicity %s is unknown. Choose one of available periodicity: %s',
|
||||
$periodicity->name,
|
||||
join(', ', $instervals)
|
||||
);
|
||||
|
||||
$exception = new IntervalException($message, $code, $previous);
|
||||
$exception->periodicity = $periodicity;
|
||||
$exception->availableIntervals = $instervals;
|
||||
return $exception;
|
||||
}
|
||||
}
|
120
tests/Support/Calendar/CalculatorProvider.php
Normal file
120
tests/Support/Calendar/CalculatorProvider.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Tests\Support\Calendar;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Support\Calendar\Periodicity;
|
||||
use Tests\Support\Calendar\Periodicity\IntervalProvider;
|
||||
|
||||
readonly class CalculatorProvider
|
||||
{
|
||||
public IntervalProvider $intervalProvider;
|
||||
public Periodicity $periodicity;
|
||||
public string $label;
|
||||
public int $skip;
|
||||
|
||||
private function __construct(IntervalProvider $intervalProvider, Periodicity $periodicity, int $skip = 0)
|
||||
{
|
||||
$this->skip = $skip;
|
||||
$this->intervalProvider = $intervalProvider;
|
||||
$this->periodicity = $periodicity;
|
||||
$this->label = "{$periodicity->name} {$intervalProvider->label}";
|
||||
}
|
||||
|
||||
public static function from(Periodicity $periodicity, IntervalProvider $interval, int $skip = 0): CalculatorProvider
|
||||
{
|
||||
return new self($interval, $periodicity, $skip);
|
||||
}
|
||||
|
||||
public function epoch(): Carbon
|
||||
{
|
||||
return $this->intervalProvider->epoch;
|
||||
}
|
||||
|
||||
public function expected(): Carbon
|
||||
{
|
||||
return $this->intervalProvider->expected;
|
||||
}
|
||||
|
||||
public static function providePeriodicityWithSkippedIntervals(): \Generator
|
||||
{
|
||||
$intervals = [
|
||||
CalculatorProvider::from(Periodicity::Daily, new IntervalProvider(Carbon::now(), Carbon::now()->addDays(2)), 1),
|
||||
CalculatorProvider::from(Periodicity::Daily, new IntervalProvider(Carbon::now(), Carbon::now()->addDays(3)), 2),
|
||||
CalculatorProvider::from(Periodicity::Daily, new IntervalProvider(Carbon::parse('2023-01-31'), Carbon::parse('2023-02-11')), 10),
|
||||
|
||||
CalculatorProvider::from(Periodicity::Weekly, new IntervalProvider(Carbon::now(), Carbon::now()->addWeeks(3)), 2),
|
||||
CalculatorProvider::from(Periodicity::Weekly, new IntervalProvider(Carbon::parse('2023-01-31'), Carbon::parse('2023-02-14')), 1),
|
||||
|
||||
CalculatorProvider::from(Periodicity::Fortnightly, new IntervalProvider(Carbon::now(), Carbon::now()->addWeeks(4)), 1),
|
||||
CalculatorProvider::from(Periodicity::Fortnightly, new IntervalProvider(Carbon::parse('2023-01-29'), Carbon::parse('2023-02-26')), 1),
|
||||
CalculatorProvider::from(Periodicity::Fortnightly, new IntervalProvider(Carbon::parse('2023-01-30'), Carbon::parse('2023-02-27')), 1),
|
||||
CalculatorProvider::from(Periodicity::Fortnightly, new IntervalProvider(Carbon::parse('2023-01-31'), Carbon::parse('2023-02-28')), 1),
|
||||
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::now(), Carbon::now()->addMonthsNoOverflow(2)), 1),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2019-12-30'), Carbon::parse('2020-02-29')), 1),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2019-12-31'), Carbon::parse('2020-02-29')), 1),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2020-01-29'), Carbon::parse('2020-03-29')), 1),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2020-01-31'), Carbon::parse('2020-09-30')), 7),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2020-12-29'), Carbon::parse('2021-02-28')), 1),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2020-12-30'), Carbon::parse('2021-02-28')), 1),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2020-12-31'), Carbon::parse('2021-02-28')), 1),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2023-03-31'), Carbon::parse('2023-11-30')), 7),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2023-05-31'), Carbon::parse('2023-08-31')), 2),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2023-07-31'), Carbon::parse('2023-09-30')), 1),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2023-10-30'), Carbon::parse('2024-02-29')), 3),
|
||||
CalculatorProvider::from(Periodicity::Monthly, new IntervalProvider(Carbon::parse('2023-10-31'), Carbon::parse('2024-02-29')), 3),
|
||||
|
||||
CalculatorProvider::from(Periodicity::Quarterly, new IntervalProvider(Carbon::now(), Carbon::now()->addMonthsNoOverflow(9)), 2),
|
||||
CalculatorProvider::from(Periodicity::Quarterly, new IntervalProvider(Carbon::parse('2019-05-29'), Carbon::parse('2020-02-29')), 2),
|
||||
CalculatorProvider::from(Periodicity::Quarterly, new IntervalProvider(Carbon::parse('2019-05-30'), Carbon::parse('2020-02-29')), 2),
|
||||
CalculatorProvider::from(Periodicity::Quarterly, new IntervalProvider(Carbon::parse('2019-05-31'), Carbon::parse('2020-02-29')), 2),
|
||||
CalculatorProvider::from(Periodicity::Quarterly, new IntervalProvider(Carbon::parse('2020-02-29'), Carbon::parse('2021-02-28')), 3),
|
||||
CalculatorProvider::from(Periodicity::Quarterly, new IntervalProvider(Carbon::parse('2020-08-29'), Carbon::parse('2021-02-28')), 1),
|
||||
CalculatorProvider::from(Periodicity::Quarterly, new IntervalProvider(Carbon::parse('2020-08-30'), Carbon::parse('2021-02-28')), 1),
|
||||
CalculatorProvider::from(Periodicity::Quarterly, new IntervalProvider(Carbon::parse('2020-08-31'), Carbon::parse('2021-02-28')), 1),
|
||||
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::now(), Carbon::now()->addMonthsNoOverflow(12)), 1),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::now(), Carbon::now()->addMonthsNoOverflow(18)), 2),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::now(), Carbon::now()->addMonthsNoOverflow(24)), 3),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2018-08-29'), Carbon::parse('2020-02-29')), 2),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2018-08-30'), Carbon::parse('2020-02-29')), 2),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2018-08-31'), Carbon::parse('2020-02-29')), 2),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2019-01-31'), Carbon::parse('2021-01-31')), 3),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2019-02-28'), Carbon::parse('2021-08-28')), 4),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2020-01-31'), Carbon::parse('2021-01-31')), 1),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2020-02-29'), Carbon::parse('2021-02-28')), 1),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2020-08-29'), Carbon::parse('2022-02-28')), 2),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2020-08-30'), Carbon::parse('2022-02-28')), 2),
|
||||
CalculatorProvider::from(Periodicity::HalfYearly, new IntervalProvider(Carbon::parse('2020-08-31'), Carbon::parse('2022-02-28')), 2),
|
||||
|
||||
CalculatorProvider::from(Periodicity::Yearly, new IntervalProvider(Carbon::now(), Carbon::now()->addYearsNoOverflow(3)), 2),
|
||||
CalculatorProvider::from(Periodicity::Yearly, new IntervalProvider(Carbon::parse('2019-01-29'), Carbon::parse('2025-01-29')), 5),
|
||||
CalculatorProvider::from(Periodicity::Yearly, new IntervalProvider(Carbon::parse('2020-02-29'), Carbon::parse('2031-02-28')), 10),
|
||||
];
|
||||
|
||||
/** @var IntervalProvider $interval */
|
||||
foreach ($intervals as $index => $interval) {
|
||||
yield "#{$index} {$interval->label}" => [$interval];
|
||||
}
|
||||
}
|
||||
}
|
97
tests/Support/Calendar/CalculatorTest.php
Normal file
97
tests/Support/Calendar/CalculatorTest.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Tests\Support\Calendar;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Api\V1\Controllers\Insight\Income\PeriodController;
|
||||
use FireflyIII\Support\Calendar\Calculator;
|
||||
use FireflyIII\Support\Calendar\Exceptions\IntervalException;
|
||||
use FireflyIII\Support\Calendar\Periodicity;
|
||||
use FireflyIII\Support\Navigation;
|
||||
use Tests\Support\Calendar\Periodicity\DailyTest;
|
||||
use Tests\Support\Calendar\Periodicity\FortnightlyTest;
|
||||
use Tests\Support\Calendar\Periodicity\HalfYearlyTest;
|
||||
use Tests\Support\Calendar\Periodicity\IntervalProvider;
|
||||
use Tests\Support\Calendar\Periodicity\MonthlyTest;
|
||||
use Tests\Support\Calendar\Periodicity\QuarterlyTest;
|
||||
use Tests\Support\Calendar\Periodicity\WeeklyTest;
|
||||
use Tests\Support\Calendar\Periodicity\YearlyTest;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CalculatorTest extends TestCase
|
||||
{
|
||||
private static function convert(Periodicity $periodicity, array $intervals): array
|
||||
{
|
||||
$periodicityIntervals = [];
|
||||
/** @var IntervalProvider $interval */
|
||||
foreach ($intervals as $index => $interval) {
|
||||
$calculator = CalculatorProvider::from($periodicity, $interval);
|
||||
|
||||
$periodicityIntervals["#{$index} {$calculator->label}"] = [$calculator];
|
||||
}
|
||||
return $periodicityIntervals;
|
||||
}
|
||||
|
||||
public static function provideAllPeriodicity(): \Generator
|
||||
{
|
||||
$intervals = [];
|
||||
$intervals = array_merge($intervals, self::convert(Periodicity::Daily, DailyTest::provideIntervals()));
|
||||
$intervals = array_merge($intervals, self::convert(Periodicity::Weekly, WeeklyTest::provideIntervals()));
|
||||
$intervals = array_merge($intervals, self::convert(Periodicity::Fortnightly, FortnightlyTest::provideIntervals()));
|
||||
$intervals = array_merge($intervals, self::convert(Periodicity::Monthly, MonthlyTest::provideIntervals()));
|
||||
$intervals = array_merge($intervals, self::convert(Periodicity::Quarterly, QuarterlyTest::provideIntervals()));
|
||||
$intervals = array_merge($intervals, self::convert(Periodicity::HalfYearly, HalfYearlyTest::provideIntervals()));
|
||||
$intervals = array_merge($intervals, self::convert(Periodicity::Yearly, YearlyTest::provideIntervals()));
|
||||
|
||||
/** @var IntervalProvider $interval */
|
||||
foreach ($intervals as $label => $interval) {
|
||||
yield $label => $interval;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideAllPeriodicity
|
||||
* @throws IntervalException
|
||||
*/
|
||||
public function testGivenADailyPeriodicityWhenCallTheNextDateByIntervalMethodThenReturnsTheExpectedDateSuccessful(CalculatorProvider $provider)
|
||||
{
|
||||
$calculator = new Calculator();
|
||||
$period = $calculator->nextDateByInterval($provider->epoch(), $provider->periodicity);
|
||||
$this->assertEquals($provider->expected()->toDateString(), $period->toDateString());
|
||||
}
|
||||
|
||||
public static function provideSkippedIntervals(): \Generator
|
||||
{
|
||||
return CalculatorProvider::providePeriodicityWithSkippedIntervals();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSkippedIntervals
|
||||
* @throws IntervalException
|
||||
*/
|
||||
public function testGivenAnEpochWithSkipIntervalNumberWhenCallTheNextDateBySkippedIntervalMethodThenReturnsTheExpectedDateSuccessful(CalculatorProvider $provider)
|
||||
{
|
||||
$calculator = new Calculator();
|
||||
$period = $calculator->nextDateByInterval($provider->epoch(), $provider->periodicity, $provider->skip);
|
||||
$this->assertEquals($provider->expected()->toDateString(), $period->toDateString());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user