diff --git a/app/Repositories/Recurring/RecurringRepository.php b/app/Repositories/Recurring/RecurringRepository.php index 7cf6fbba9b..f54b808abf 100644 --- a/app/Repositories/Recurring/RecurringRepository.php +++ b/app/Repositories/Recurring/RecurringRepository.php @@ -41,6 +41,7 @@ use FireflyIII\Services\Internal\Destroy\RecurrenceDestroyService; use FireflyIII\Services\Internal\Update\RecurrenceUpdateService; use FireflyIII\Support\Repositories\Recurring\CalculateRangeOccurrences; use FireflyIII\Support\Repositories\Recurring\CalculateXOccurrences; +use FireflyIII\Support\Repositories\Recurring\CalculateXOccurrencesSince; use FireflyIII\Support\Repositories\Recurring\FiltersWeekends; use FireflyIII\User; use Illuminate\Pagination\LengthAwarePaginator; @@ -52,7 +53,7 @@ use Log; */ class RecurringRepository implements RecurringRepositoryInterface { - use CalculateRangeOccurrences, CalculateXOccurrences, FiltersWeekends; + use CalculateRangeOccurrences, CalculateXOccurrences, CalculateXOccurrencesSince, FiltersWeekends; /** @var User */ private $user; @@ -496,4 +497,44 @@ class RecurringRepository implements RecurringRepositoryInterface return $service->update($recurrence, $data); } + + /** + * Calculate the next X iterations starting on the date given in $date. + * Returns an array of Carbon objects. + * + * Only returns them of they are after $afterDate + * + * @param RecurrenceRepetition $repetition + * @param Carbon $date + * @param Carbon $afterDate + * @param int $count + * + * @return array + * @throws FireflyException + */ + public function getXOccurrencesSince(RecurrenceRepetition $repetition, Carbon $date, Carbon $afterDate, int $count): array + { + $skipMod = $repetition->repetition_skip + 1; + $occurrences = []; + if ('daily' === $repetition->repetition_type) { + $occurrences = $this->getXDailyOccurrencesSince($date, $afterDate, $count, $skipMod); + } + if ('weekly' === $repetition->repetition_type) { + $occurrences = $this->getXWeeklyOccurrencesSince($date, $afterDate, $count, $skipMod, $repetition->repetition_moment); + } + if ('monthly' === $repetition->repetition_type) { + $occurrences = $this->getXMonthlyOccurrencesSince($date, $afterDate, $count, $skipMod, $repetition->repetition_moment); + } + if ('ndom' === $repetition->repetition_type) { + $occurrences = $this->getXNDomOccurrencesSince($date, $afterDate, $count, $skipMod, $repetition->repetition_moment); + } + if ('yearly' === $repetition->repetition_type) { + $occurrences = $this->getXYearlyOccurrencesSince($date, $afterDate, $count, $skipMod, $repetition->repetition_moment); + } + + // filter out all the weekend days: + $occurrences = $this->filterWeekends($repetition, $occurrences); + + return $occurrences; + } } diff --git a/app/Repositories/Recurring/RecurringRepositoryInterface.php b/app/Repositories/Recurring/RecurringRepositoryInterface.php index b18caa5e93..46f6ce175a 100644 --- a/app/Repositories/Recurring/RecurringRepositoryInterface.php +++ b/app/Repositories/Recurring/RecurringRepositoryInterface.php @@ -163,6 +163,22 @@ interface RecurringRepositoryInterface */ public function getXOccurrences(RecurrenceRepetition $repetition, Carbon $date, int $count): array; + /** + * Calculate the next X iterations starting on the date given in $date. + * Returns an array of Carbon objects. + * + * Only returns them of they are after $afterDate + * + * @param RecurrenceRepetition $repetition + * @param Carbon $date + * @param Carbon $afterDate + * @param int $count + * + * @throws FireflyException + * @return array + */ + public function getXOccurrencesSince(RecurrenceRepetition $repetition, Carbon $date,Carbon $afterDate, int $count): array; + /** * Parse the repetition in a string that is user readable. * diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php new file mode 100644 index 0000000000..f6faf55696 --- /dev/null +++ b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php @@ -0,0 +1,225 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Repositories\Recurring; + + +use Carbon\Carbon; + +/** + * Class CalculateXOccurrencesSince + */ +trait CalculateXOccurrencesSince +{ + + /** + * Calculates the number of daily occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip + * over $skipMod -1 recurrences. + * + * @param Carbon $date + * @param Carbon $afterDate + * @param int $count + * @param int $skipMod + * + * @return array + */ + protected function getXDailyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod): array + { + $return = []; + $mutator = clone $date; + $total = 0; + $attempts = 0; + while ($total < $count) { + if (0 === $attempts % $skipMod && $mutator->gt($afterDate)) { + $return[] = clone $mutator; + $total++; + } + $mutator->addDay(); + $attempts++; + } + + return $return; + } + + + /** + * Calculates the number of monthly occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip + * over $skipMod -1 recurrences. + * + * @param Carbon $date + * @param Carbon $afterDate + * @param int $count + * @param int $skipMod + * @param string $moment + * + * @return array + */ + protected function getXMonthlyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array + { + $return = []; + $mutator = clone $date; + $total = 0; + $attempts = 0; + $dayOfMonth = (int)$moment; + if ($mutator->day > $dayOfMonth) { + // day has passed already, add a month. + $mutator->addMonth(); + } + + while ($total < $count) { + $domCorrected = min($dayOfMonth, $mutator->daysInMonth); + $mutator->day = $domCorrected; + if (0 === $attempts % $skipMod && $mutator->gte($afterDate)) { + $return[] = clone $mutator; + $total++; + } + $attempts++; + $mutator->endOfMonth()->addDay(); + } + + return $return; + } + + + /** + * Calculates the number of NDOM occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip + * over $skipMod -1 recurrences. + * + * @param Carbon $date + * @param Carbon $afterDate + * @param int $count + * @param int $skipMod + * @param string $moment + * + * @return array + */ + protected function getXNDomOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array + { + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; + $mutator->addDay(); // always assume today has passed. + $mutator->startOfMonth(); + // this feels a bit like a cop out but why reinvent the wheel? + $counters = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth',]; + $daysOfWeek = [1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', 7 => 'Sunday',]; + $parts = explode(',', $moment); + + while ($total < $count) { + $string = sprintf('%s %s of %s %s', $counters[$parts[0]], $daysOfWeek[$parts[1]], $mutator->format('F'), $mutator->format('Y')); + $newCarbon = new Carbon($string); + if (0 === $attempts % $skipMod && $mutator->gte($afterDate)) { + $return[] = clone $newCarbon; + $total++; + } + $attempts++; + $mutator->endOfMonth()->addDay(); + } + + return $return; + } + + + /** + * Calculates the number of weekly occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip + * over $skipMod -1 recurrences. + * + * @param Carbon $date + * @param Carbon $afterDate + * @param int $count + * @param int $skipMod + * @param string $moment + * + * @return array + */ + protected function getXWeeklyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array + { + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; + // monday = 1 + // sunday = 7 + $mutator->addDay(); // always assume today has passed. + $dayOfWeek = (int)$moment; + if ($mutator->dayOfWeekIso > $dayOfWeek) { + // day has already passed this week, add one week: + $mutator->addWeek(); + } + // today is wednesday (3), expected is friday (5): add two days. + // today is friday (5), expected is monday (1), subtract four days. + $dayDifference = $dayOfWeek - $mutator->dayOfWeekIso; + $mutator->addDays($dayDifference); + + while ($total < $count) { + if (0 === $attempts % $skipMod && $mutator->gte($afterDate)) { + $return[] = clone $mutator; + $total++; + } + $attempts++; + $mutator->addWeek(); + } + + return $return; + } + + + /** + * Calculates the number of yearly occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip + * over $skipMod -1 recurrences. + * + * @param Carbon $date + * @param Carbon $afterDate + * @param int $count + * @param int $skipMod + * @param string $moment + * + * @return array + */ + protected function getXYearlyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array + { + $return = []; + $mutator = clone $date; + $total = 0; + $attempts = 0; + $date = new Carbon($moment); + $date->year = $mutator->year; + if ($mutator > $date) { + $date->addYear(); + } + $obj = clone $date; + while ($total < $count) { + if (0 === $attempts % $skipMod && $mutator->gte($afterDate)) { + $return[] = clone $obj; + $total++; + } + $obj->addYears(1); + $attempts++; + } + + return $return; + + } +} diff --git a/app/Transformers/RecurrenceTransformer.php b/app/Transformers/RecurrenceTransformer.php index 4bb67238e7..7118016721 100644 --- a/app/Transformers/RecurrenceTransformer.php +++ b/app/Transformers/RecurrenceTransformer.php @@ -125,9 +125,6 @@ class RecurrenceTransformer extends AbstractTransformer private function getRepetitions(Recurrence $recurrence): array { $fromDate = $recurrence->latest_date ?? $recurrence->first_date; - // date in the past? use today: - $today = new Carbon; - $fromDate = $fromDate->lte($today) ? $today : $fromDate; $return = []; /** @var RecurrenceRepetition $repetition */ @@ -145,7 +142,7 @@ class RecurrenceTransformer extends AbstractTransformer ]; // get the (future) occurrences for this specific type of repetition: - $occurrences = $this->repository->getXOccurrences($repetition, $fromDate, 5); + $occurrences = $this->repository->getXOccurrencesSince($repetition, $fromDate, new Carbon, 5); /** @var Carbon $carbon */ foreach ($occurrences as $carbon) { $repetitionArray['occurrences'][] = $carbon->format('Y-m-d');