From 4e3c2ba72c330ff2c63677893bac84a7e62b40d7 Mon Sep 17 00:00:00 2001 From: Antonio Spinelli Date: Sun, 2 Jul 2023 23:33:11 -0300 Subject: [PATCH] Calculate the next date using periodicity strategies. All these strategies encapsulate how the Carbon library adds the interval to the current date. Monthly, Quarterly, Half-Yearly, and Yearly explicitly use the overflow control to guarantee the end of the next month or year adequately. --- app/Support/Calendar/Periodicity.php | 33 ++++++++++++ app/Support/Calendar/Periodicity/Daily.php | 32 ++++++++++++ .../Calendar/Periodicity/Fortnightly.php | 27 ++++++++++ .../Calendar/Periodicity/HalfYearly.php | 27 ++++++++++ .../Calendar/Periodicity/Interspacable.php | 29 +++++++++++ app/Support/Calendar/Periodicity/Interval.php | 32 ++++++++++++ app/Support/Calendar/Periodicity/Monthly.php | 32 ++++++++++++ .../Calendar/Periodicity/Quarterly.php | 27 ++++++++++ app/Support/Calendar/Periodicity/Weekly.php | 32 ++++++++++++ app/Support/Calendar/Periodicity/Yearly.php | 32 ++++++++++++ .../Calendar/Periodicity/DailyTest.php | 42 +++++++++++++++ .../Calendar/Periodicity/FortnightlyTest.php | 42 +++++++++++++++ .../Calendar/Periodicity/HalfYearlyTest.php | 50 ++++++++++++++++++ .../Calendar/Periodicity/IntervalProvider.php | 38 ++++++++++++++ .../Calendar/Periodicity/IntervalTestCase.php | 52 +++++++++++++++++++ .../Calendar/Periodicity/MonthlyTest.php | 52 +++++++++++++++++++ .../Calendar/Periodicity/QuarterlyTest.php | 49 +++++++++++++++++ .../Calendar/Periodicity/WeeklyTest.php | 42 +++++++++++++++ .../Calendar/Periodicity/YearlyTest.php | 43 +++++++++++++++ 19 files changed, 713 insertions(+) create mode 100644 app/Support/Calendar/Periodicity.php create mode 100644 app/Support/Calendar/Periodicity/Daily.php create mode 100644 app/Support/Calendar/Periodicity/Fortnightly.php create mode 100644 app/Support/Calendar/Periodicity/HalfYearly.php create mode 100644 app/Support/Calendar/Periodicity/Interspacable.php create mode 100644 app/Support/Calendar/Periodicity/Interval.php create mode 100644 app/Support/Calendar/Periodicity/Monthly.php create mode 100644 app/Support/Calendar/Periodicity/Quarterly.php create mode 100644 app/Support/Calendar/Periodicity/Weekly.php create mode 100644 app/Support/Calendar/Periodicity/Yearly.php create mode 100644 tests/Support/Calendar/Periodicity/DailyTest.php create mode 100644 tests/Support/Calendar/Periodicity/FortnightlyTest.php create mode 100644 tests/Support/Calendar/Periodicity/HalfYearlyTest.php create mode 100644 tests/Support/Calendar/Periodicity/IntervalProvider.php create mode 100644 tests/Support/Calendar/Periodicity/IntervalTestCase.php create mode 100644 tests/Support/Calendar/Periodicity/MonthlyTest.php create mode 100644 tests/Support/Calendar/Periodicity/QuarterlyTest.php create mode 100644 tests/Support/Calendar/Periodicity/WeeklyTest.php create mode 100644 tests/Support/Calendar/Periodicity/YearlyTest.php diff --git a/app/Support/Calendar/Periodicity.php b/app/Support/Calendar/Periodicity.php new file mode 100644 index 0000000000..05825e19a1 --- /dev/null +++ b/app/Support/Calendar/Periodicity.php @@ -0,0 +1,33 @@ +. + */ + +namespace FireflyIII\Support\Calendar; + +enum Periodicity +{ + case Daily; + case Weekly; + case Fortnightly; + case Monthly; + case Quarterly; + case HalfYearly; + case Yearly; +} diff --git a/app/Support/Calendar/Periodicity/Daily.php b/app/Support/Calendar/Periodicity/Daily.php new file mode 100644 index 0000000000..39fc34196d --- /dev/null +++ b/app/Support/Calendar/Periodicity/Daily.php @@ -0,0 +1,32 @@ +. + */ + +namespace FireflyIII\Support\Calendar\Periodicity; + +use Carbon\Carbon; + +final class Daily extends Interval +{ + public function nextDate(Carbon $date, int $interval = 1): Carbon + { + return ($date->clone())->addDays($this->skip($interval)); + } +} diff --git a/app/Support/Calendar/Periodicity/Fortnightly.php b/app/Support/Calendar/Periodicity/Fortnightly.php new file mode 100644 index 0000000000..a64880a058 --- /dev/null +++ b/app/Support/Calendar/Periodicity/Fortnightly.php @@ -0,0 +1,27 @@ +. + */ + +namespace FireflyIII\Support\Calendar\Periodicity; + +final class Fortnightly extends Weekly +{ + const INTERVAL = 2; +} diff --git a/app/Support/Calendar/Periodicity/HalfYearly.php b/app/Support/Calendar/Periodicity/HalfYearly.php new file mode 100644 index 0000000000..e8d6242bb2 --- /dev/null +++ b/app/Support/Calendar/Periodicity/HalfYearly.php @@ -0,0 +1,27 @@ +. + */ + +namespace FireflyIII\Support\Calendar\Periodicity; + +final class HalfYearly extends Monthly +{ + const INTERVAL = 6; +} diff --git a/app/Support/Calendar/Periodicity/Interspacable.php b/app/Support/Calendar/Periodicity/Interspacable.php new file mode 100644 index 0000000000..a10658cdfc --- /dev/null +++ b/app/Support/Calendar/Periodicity/Interspacable.php @@ -0,0 +1,29 @@ +. + */ + +namespace FireflyIII\Support\Calendar\Periodicity; + +use Carbon\Carbon; + +interface Interspacable +{ + public function nextDate(Carbon $date, int $interval = 1): Carbon; +} diff --git a/app/Support/Calendar/Periodicity/Interval.php b/app/Support/Calendar/Periodicity/Interval.php new file mode 100644 index 0000000000..73db1f0409 --- /dev/null +++ b/app/Support/Calendar/Periodicity/Interval.php @@ -0,0 +1,32 @@ +. + */ + +namespace FireflyIII\Support\Calendar\Periodicity; + +abstract class Interval implements Interspacable +{ + const INTERVAL = 1; + + public function skip(int $skip): int + { + return static::INTERVAL * $skip; + } +} diff --git a/app/Support/Calendar/Periodicity/Monthly.php b/app/Support/Calendar/Periodicity/Monthly.php new file mode 100644 index 0000000000..34cc9bf3e6 --- /dev/null +++ b/app/Support/Calendar/Periodicity/Monthly.php @@ -0,0 +1,32 @@ +. + */ + +namespace FireflyIII\Support\Calendar\Periodicity; + +use Carbon\Carbon; + +class Monthly extends Interval +{ + public function nextDate(Carbon $date, int $interval = 1): Carbon + { + return ($date->clone())->addMonthsNoOverflow($this->skip($interval)); + } +} diff --git a/app/Support/Calendar/Periodicity/Quarterly.php b/app/Support/Calendar/Periodicity/Quarterly.php new file mode 100644 index 0000000000..b20dc60250 --- /dev/null +++ b/app/Support/Calendar/Periodicity/Quarterly.php @@ -0,0 +1,27 @@ +. + */ + +namespace FireflyIII\Support\Calendar\Periodicity; + +final class Quarterly extends Monthly +{ + const INTERVAL = 3; +} diff --git a/app/Support/Calendar/Periodicity/Weekly.php b/app/Support/Calendar/Periodicity/Weekly.php new file mode 100644 index 0000000000..519ea143d8 --- /dev/null +++ b/app/Support/Calendar/Periodicity/Weekly.php @@ -0,0 +1,32 @@ +. + */ + +namespace FireflyIII\Support\Calendar\Periodicity; + +use Carbon\Carbon; + +class Weekly extends Interval +{ + public function nextDate(Carbon $date, int $interval = 1): Carbon + { + return ($date->clone())->addWeeks($this->skip($interval)); + } +} diff --git a/app/Support/Calendar/Periodicity/Yearly.php b/app/Support/Calendar/Periodicity/Yearly.php new file mode 100644 index 0000000000..768f3d87b3 --- /dev/null +++ b/app/Support/Calendar/Periodicity/Yearly.php @@ -0,0 +1,32 @@ +. + */ + +namespace FireflyIII\Support\Calendar\Periodicity; + +use Carbon\Carbon; + +final class Yearly extends Interval +{ + public function nextDate(Carbon $date, int $interval = 1): Carbon + { + return ($date->clone())->addYearsNoOverflow($this->skip($interval)); + } +} diff --git a/tests/Support/Calendar/Periodicity/DailyTest.php b/tests/Support/Calendar/Periodicity/DailyTest.php new file mode 100644 index 0000000000..b4f25f2e5e --- /dev/null +++ b/tests/Support/Calendar/Periodicity/DailyTest.php @@ -0,0 +1,42 @@ +. + */ + +namespace Tests\Support\Calendar\Periodicity; + +use Carbon\Carbon; +use FireflyIII\Support\Calendar\Periodicity; +use FireflyIII\Support\Calendar\Periodicity\Interval; + +class DailyTest extends IntervalTestCase +{ + public static function factory(): Interval + { + return new Periodicity\Daily(); + } + + public static function provideIntervals(): array + { + return [ + new IntervalProvider(Carbon::now(), Carbon::tomorrow()), + new IntervalProvider(Carbon::parse('2023-01-31'), Carbon::parse('2023-02-01')), + ]; + } +} diff --git a/tests/Support/Calendar/Periodicity/FortnightlyTest.php b/tests/Support/Calendar/Periodicity/FortnightlyTest.php new file mode 100644 index 0000000000..e28b818e92 --- /dev/null +++ b/tests/Support/Calendar/Periodicity/FortnightlyTest.php @@ -0,0 +1,42 @@ +. + */ + +namespace Tests\Support\Calendar\Periodicity; + +use Carbon\Carbon; +use FireflyIII\Support\Calendar\Periodicity; +use FireflyIII\Support\Calendar\Periodicity\Interval; + +class FortnightlyTest extends IntervalTestCase +{ + public static function factory(): Interval + { + return new Periodicity\Fortnightly(); + } + + public static function provideIntervals(): array + { + return [ + new IntervalProvider(Carbon::now(), Carbon::now()->addWeeks(2)), + new IntervalProvider(Carbon::parse('2023-01-31'), Carbon::parse('2023-02-14')), + ]; + } +} diff --git a/tests/Support/Calendar/Periodicity/HalfYearlyTest.php b/tests/Support/Calendar/Periodicity/HalfYearlyTest.php new file mode 100644 index 0000000000..08da7027c7 --- /dev/null +++ b/tests/Support/Calendar/Periodicity/HalfYearlyTest.php @@ -0,0 +1,50 @@ +. + */ + +namespace Tests\Support\Calendar\Periodicity; + +use Carbon\Carbon; +use FireflyIII\Support\Calendar\Periodicity; +use FireflyIII\Support\Calendar\Periodicity\Interval; + +class HalfYearlyTest extends IntervalTestCase +{ + public static function factory(): Interval + { + return new Periodicity\HalfYearly(); + } + + public static function provideIntervals(): array + { + return [ + new IntervalProvider(Carbon::now(), Carbon::now()->addMonthsNoOverflow(6)), + new IntervalProvider(Carbon::parse('2019-01-29'), Carbon::parse('2019-07-29')), + new IntervalProvider(Carbon::parse('2019-01-30'), Carbon::parse('2019-07-30')), + new IntervalProvider(Carbon::parse('2019-01-31'), Carbon::parse('2019-07-31')), + new IntervalProvider(Carbon::parse('2018-11-01'), Carbon::parse('2019-05-01')), + new IntervalProvider(Carbon::parse('2019-08-29'), Carbon::parse('2020-02-29')), + new IntervalProvider(Carbon::parse('2019-08-30'), Carbon::parse('2020-02-29')), + new IntervalProvider(Carbon::parse('2019-08-31'), Carbon::parse('2020-02-29')), + new IntervalProvider(Carbon::parse('2020-08-29'), Carbon::parse('2021-02-28')), + new IntervalProvider(Carbon::parse('2020-08-30'), Carbon::parse('2021-02-28')), + ]; + } +} diff --git a/tests/Support/Calendar/Periodicity/IntervalProvider.php b/tests/Support/Calendar/Periodicity/IntervalProvider.php new file mode 100644 index 0000000000..4725885d26 --- /dev/null +++ b/tests/Support/Calendar/Periodicity/IntervalProvider.php @@ -0,0 +1,38 @@ +. + */ + +namespace Tests\Support\Calendar\Periodicity; + +use Carbon\Carbon; + +readonly class IntervalProvider +{ + public Carbon $epoch; + public Carbon $expected; + public string $label; + + public function __construct(Carbon $epoch, Carbon $expected) + { + $this->epoch = $epoch; + $this->expected = $expected; + $this->label = "given {$epoch->toDateString()} expects {$expected->toDateString()}"; + } +} diff --git a/tests/Support/Calendar/Periodicity/IntervalTestCase.php b/tests/Support/Calendar/Periodicity/IntervalTestCase.php new file mode 100644 index 0000000000..d5db1d901e --- /dev/null +++ b/tests/Support/Calendar/Periodicity/IntervalTestCase.php @@ -0,0 +1,52 @@ +. + */ + +namespace Tests\Support\Calendar\Periodicity; + +use FireflyIII\Support\Calendar\Periodicity\Interval; +use Tests\TestCase; + +abstract class IntervalTestCase extends TestCase +{ + abstract public static function factory(): Interval; + + public abstract static function provideIntervals(): array; + + public static function provider(): \Generator + { + $intervals = static::provideIntervals(); + /** @var IntervalProvider $interval */ + foreach ($intervals as $interval) { + yield "{$interval->label}" => [$interval]; + } + } + + /** + * @dataProvider provider + * @param IntervalProvider $provider + * @return void + */ + public function testGivenAnEpochWhenCallTheNextDateThenReturnsTheExpectedDateSuccessful(IntervalProvider $provider): void + { + $period = static::factory()->nextDate($provider->epoch); + $this->assertEquals($provider->expected->toDateString(), $period->toDateString()); + } +} diff --git a/tests/Support/Calendar/Periodicity/MonthlyTest.php b/tests/Support/Calendar/Periodicity/MonthlyTest.php new file mode 100644 index 0000000000..fab8785a4a --- /dev/null +++ b/tests/Support/Calendar/Periodicity/MonthlyTest.php @@ -0,0 +1,52 @@ +. + */ + +namespace Tests\Support\Calendar\Periodicity; + +use Carbon\Carbon; +use FireflyIII\Support\Calendar\Periodicity; +use FireflyIII\Support\Calendar\Periodicity\Interval; + +class MonthlyTest extends IntervalTestCase +{ + public static function factory(): Interval + { + return new Periodicity\Monthly(); + } + + public static function provideIntervals(): array + { + return [ + new IntervalProvider(Carbon::now(), Carbon::now()->addMonth(1)), + new IntervalProvider(Carbon::parse('2019-01-01'), Carbon::parse('2019-02-01')), + new IntervalProvider(Carbon::parse('2020-01-29'), Carbon::parse('2020-02-29')), + new IntervalProvider(Carbon::parse('2020-01-30'), Carbon::parse('2020-02-29')), + new IntervalProvider(Carbon::parse('2020-01-31'), Carbon::parse('2020-02-29')), + new IntervalProvider(Carbon::parse('2021-01-29'), Carbon::parse('2021-02-28')), + new IntervalProvider(Carbon::parse('2021-01-30'), Carbon::parse('2021-02-28')), + new IntervalProvider(Carbon::parse('2021-01-31'), Carbon::parse('2021-02-28')), + new IntervalProvider(Carbon::parse('2023-03-31'), Carbon::parse('2023-04-30')), + new IntervalProvider(Carbon::parse('2023-05-31'), Carbon::parse('2023-06-30')), + new IntervalProvider(Carbon::parse('2023-08-31'), Carbon::parse('2023-09-30')), + new IntervalProvider(Carbon::parse('2023-10-31'), Carbon::parse('2023-11-30')), + ]; + } +} diff --git a/tests/Support/Calendar/Periodicity/QuarterlyTest.php b/tests/Support/Calendar/Periodicity/QuarterlyTest.php new file mode 100644 index 0000000000..22a527e109 --- /dev/null +++ b/tests/Support/Calendar/Periodicity/QuarterlyTest.php @@ -0,0 +1,49 @@ +. + */ + +namespace Tests\Support\Calendar\Periodicity; + +use Carbon\Carbon; +use FireflyIII\Support\Calendar\Periodicity; +use FireflyIII\Support\Calendar\Periodicity\Interval; + +class QuarterlyTest extends IntervalTestCase +{ + public static function factory(): Interval + { + return new Periodicity\Quarterly(); + } + + public static function provideIntervals(): array + { + return [ + new IntervalProvider(Carbon::now(), Carbon::now()->addMonths(3)), + new IntervalProvider(Carbon::parse('2019-01-29'), Carbon::parse('2019-04-29')), + new IntervalProvider(Carbon::parse('2019-01-30'), Carbon::parse('2019-04-30')), + new IntervalProvider(Carbon::parse('2019-01-31'), Carbon::parse('2019-04-30')), + new IntervalProvider(Carbon::parse('2018-11-01'), Carbon::parse('2019-02-01')), + new IntervalProvider(Carbon::parse('2019-11-29'), Carbon::parse('2020-02-29')), + new IntervalProvider(Carbon::parse('2019-11-30'), Carbon::parse('2020-02-29')), + new IntervalProvider(Carbon::parse('2020-11-29'), Carbon::parse('2021-02-28')), + new IntervalProvider(Carbon::parse('2020-11-30'), Carbon::parse('2021-02-28')), + ]; + } +} diff --git a/tests/Support/Calendar/Periodicity/WeeklyTest.php b/tests/Support/Calendar/Periodicity/WeeklyTest.php new file mode 100644 index 0000000000..b889e977cd --- /dev/null +++ b/tests/Support/Calendar/Periodicity/WeeklyTest.php @@ -0,0 +1,42 @@ +. + */ + +namespace Tests\Support\Calendar\Periodicity; + +use Carbon\Carbon; +use FireflyIII\Support\Calendar\Periodicity; +use FireflyIII\Support\Calendar\Periodicity\Interval; + +class WeeklyTest extends IntervalTestCase +{ + public static function factory(): Interval + { + return new Periodicity\Weekly(); + } + + public static function provideIntervals(): array + { + return [ + new IntervalProvider(Carbon::now(), Carbon::now()->addWeek()), + new IntervalProvider(Carbon::parse('2023-01-31'), Carbon::parse('2023-02-07')), + ]; + } +} diff --git a/tests/Support/Calendar/Periodicity/YearlyTest.php b/tests/Support/Calendar/Periodicity/YearlyTest.php new file mode 100644 index 0000000000..b7f143fd62 --- /dev/null +++ b/tests/Support/Calendar/Periodicity/YearlyTest.php @@ -0,0 +1,43 @@ +. + */ + +namespace Tests\Support\Calendar\Periodicity; + +use Carbon\Carbon; +use FireflyIII\Support\Calendar\Periodicity; +use FireflyIII\Support\Calendar\Periodicity\Interval; + +class YearlyTest extends IntervalTestCase +{ + public static function factory(): Interval + { + return new Periodicity\Yearly(); + } + + public static function provideIntervals(): array + { + return [ + new IntervalProvider(Carbon::now(), Carbon::now()->addYears(1)), + new IntervalProvider(Carbon::parse('2019-01-29'), Carbon::parse('2020-01-29')), + new IntervalProvider(Carbon::parse('2020-02-29'), Carbon::parse('2021-02-28')), + ]; + } +}